From 8309693fd005823a1346590e941b4b78beef4ac1 Mon Sep 17 00:00:00 2001 From: KiWan KIM Date: Sat, 6 Jul 2024 15:53:02 +0900 Subject: [PATCH] =?UTF-8?q?[friends-native]=20=EC=B9=B4=EC=B9=B4=EC=98=A4?= =?UTF-8?q?=ED=86=A1=20=EC=B9=9C=EA=B5=AC=20=EC=B6=94=EA=B0=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=B0=98=EC=98=81=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: woohm402 --- .../src/app/assets/icons/kakaotalk.svg | 4 + .../src/app/assets/icons/user-hashtag.svg | 12 ++ .../src/app/components/Icons/Kakaotalk.tsx | 4 + .../app/components/Icons/UserHashtagIcon.tsx | 4 + .../src/app/contexts/ServiceContext.ts | 3 + .../src/app/queries/useFriends.ts | 22 ++- .../src/app/queries/useRequestFriendToken.ts | 12 ++ .../index.tsx | 29 +--- .../ManageFriendsDrawerContent/index.tsx | 2 +- .../RequestFriendsMethodList/index.tsx | 80 ++++++++++ .../RequestFriendsWithNickname/index.tsx | 90 +++++++++++ .../index.tsx | 16 ++ .../src/app/screens/MainScreen/index.tsx | 144 ++++++++---------- .../infrastructures/createFriendRepository.ts | 2 + .../infrastructures/createFriendService.ts | 6 +- .../createNativeEventService.ts | 13 ++ apps/friends-react-native/src/main.tsx | 6 +- .../src/repositories/friendRepository.ts | 4 + .../src/types/native.d.ts | 11 ++ .../src/usecases/friendService.ts | 5 +- .../src/usecases/nativeEventService.ts | 10 ++ 21 files changed, 373 insertions(+), 106 deletions(-) create mode 100644 apps/friends-react-native/src/app/assets/icons/kakaotalk.svg create mode 100644 apps/friends-react-native/src/app/assets/icons/user-hashtag.svg create mode 100644 apps/friends-react-native/src/app/components/Icons/Kakaotalk.tsx create mode 100644 apps/friends-react-native/src/app/components/Icons/UserHashtagIcon.tsx create mode 100644 apps/friends-react-native/src/app/queries/useRequestFriendToken.ts create mode 100644 apps/friends-react-native/src/app/screens/MainScreen/RequestFriendsBottomSheetContent/RequestFriendsMethodList/index.tsx create mode 100644 apps/friends-react-native/src/app/screens/MainScreen/RequestFriendsBottomSheetContent/RequestFriendsWithNickname/index.tsx create mode 100644 apps/friends-react-native/src/app/screens/MainScreen/RequestFriendsBottomSheetContent/index.tsx create mode 100644 apps/friends-react-native/src/infrastructures/createNativeEventService.ts create mode 100644 apps/friends-react-native/src/types/native.d.ts create mode 100644 apps/friends-react-native/src/usecases/nativeEventService.ts diff --git a/apps/friends-react-native/src/app/assets/icons/kakaotalk.svg b/apps/friends-react-native/src/app/assets/icons/kakaotalk.svg new file mode 100644 index 0000000..9c6b81e --- /dev/null +++ b/apps/friends-react-native/src/app/assets/icons/kakaotalk.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/friends-react-native/src/app/assets/icons/user-hashtag.svg b/apps/friends-react-native/src/app/assets/icons/user-hashtag.svg new file mode 100644 index 0000000..67dbf17 --- /dev/null +++ b/apps/friends-react-native/src/app/assets/icons/user-hashtag.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/apps/friends-react-native/src/app/components/Icons/Kakaotalk.tsx b/apps/friends-react-native/src/app/components/Icons/Kakaotalk.tsx new file mode 100644 index 0000000..07fc5cd --- /dev/null +++ b/apps/friends-react-native/src/app/components/Icons/Kakaotalk.tsx @@ -0,0 +1,4 @@ +import Icon from '../../assets/icons/kakaotalk.svg'; +import { createIconComponent } from './_createIconComponent'; + +export const KakaotalkIcon = createIconComponent(Icon); diff --git a/apps/friends-react-native/src/app/components/Icons/UserHashtagIcon.tsx b/apps/friends-react-native/src/app/components/Icons/UserHashtagIcon.tsx new file mode 100644 index 0000000..beb24b0 --- /dev/null +++ b/apps/friends-react-native/src/app/components/Icons/UserHashtagIcon.tsx @@ -0,0 +1,4 @@ +import Icon from '../../assets/icons/user-hashtag.svg'; +import { createIconComponent } from './_createIconComponent'; + +export const UserHashtagIcon = createIconComponent(Icon); diff --git a/apps/friends-react-native/src/app/contexts/ServiceContext.ts b/apps/friends-react-native/src/app/contexts/ServiceContext.ts index 5420615..ba08095 100644 --- a/apps/friends-react-native/src/app/contexts/ServiceContext.ts +++ b/apps/friends-react-native/src/app/contexts/ServiceContext.ts @@ -5,6 +5,7 @@ import { ColorService } from '../../usecases/colorService'; import { CourseBookService } from '../../usecases/courseBookService'; import { FriendService } from '../../usecases/friendService'; import { TimetableViewService } from '../../usecases/timetableViewService'; +import { NativeEventService } from '../../usecases/nativeEventService'; type ServiceContext = { timetableViewService: TimetableViewService; @@ -12,7 +13,9 @@ type ServiceContext = { friendService: FriendService; courseBookService: CourseBookService; assetService: AssetService; + nativeEventService: NativeEventService; }; + export const serviceContext = createContext(null); export const useServiceContext = () => { const context = useContext(serviceContext); diff --git a/apps/friends-react-native/src/app/queries/useFriends.ts b/apps/friends-react-native/src/app/queries/useFriends.ts index 7046b81..728a5fa 100644 --- a/apps/friends-react-native/src/app/queries/useFriends.ts +++ b/apps/friends-react-native/src/app/queries/useFriends.ts @@ -1,6 +1,7 @@ -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useServiceContext } from '../contexts/ServiceContext'; +import { FriendId } from '../../entities/friend'; export const useFriends = (req: { state: 'REQUESTED' | 'REQUESTING' | 'ACTIVE' }) => { const { friendService } = useServiceContext(); @@ -9,3 +10,22 @@ export const useFriends = (req: { state: 'REQUESTED' | 'REQUESTING' | 'ACTIVE' } queryFn: ({ queryKey: [, params] }) => friendService.listFriends(params), }); }; + +export const useAcceptFriend = () => { + const queryClient = useQueryClient(); + const { friendService } = useServiceContext(); + return useMutation({ + mutationFn: (req: { type: 'NICKNAME'; friendId: FriendId } | { type: 'KAKAO'; requestToken: string }) => + friendService.acceptFriend(req), + onSuccess: () => queryClient.invalidateQueries(), + }); +}; + +export const useDeclineFriend = () => { + const queryClient = useQueryClient(); + const { friendService } = useServiceContext(); + return useMutation({ + mutationFn: (friendId: FriendId) => friendService.declineFriend({ friendId }), + onSuccess: () => queryClient.invalidateQueries(), + }); +}; diff --git a/apps/friends-react-native/src/app/queries/useRequestFriendToken.ts b/apps/friends-react-native/src/app/queries/useRequestFriendToken.ts new file mode 100644 index 0000000..b0ffd17 --- /dev/null +++ b/apps/friends-react-native/src/app/queries/useRequestFriendToken.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query'; + +import { useServiceContext } from '../contexts/ServiceContext'; + +export const useRequestFriendToken = () => { + const { friendService } = useServiceContext(); + + return useQuery({ + queryKey: ['requestFriendToken'] as const, + queryFn: () => friendService.generateToken(), + }); +}; diff --git a/apps/friends-react-native/src/app/screens/MainScreen/ManageFriendsDrawerContent/ManageFriendsDrawerContentRequestedList/index.tsx b/apps/friends-react-native/src/app/screens/MainScreen/ManageFriendsDrawerContent/ManageFriendsDrawerContentRequestedList/index.tsx index 70f94de..d0b5345 100644 --- a/apps/friends-react-native/src/app/screens/MainScreen/ManageFriendsDrawerContent/ManageFriendsDrawerContentRequestedList/index.tsx +++ b/apps/friends-react-native/src/app/screens/MainScreen/ManageFriendsDrawerContent/ManageFriendsDrawerContentRequestedList/index.tsx @@ -1,12 +1,10 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Alert, FlatList, StyleSheet, View } from 'react-native'; -import { FriendId } from '../../../../../entities/friend'; import { Button } from '../../../../components/Button'; import { EmptyView } from '../../../../components/EmptyView'; import { Typography } from '../../../../components/Typography'; import { useServiceContext } from '../../../../contexts/ServiceContext'; -import { useFriends } from '../../../../queries/useFriends'; +import { useAcceptFriend, useDeclineFriend, useFriends } from '../../../../queries/useFriends'; export const ManageFriendsDrawerContentRequestedList = () => { const { friendService } = useServiceContext(); @@ -36,7 +34,12 @@ export const ManageFriendsDrawerContentRequestedList = () => { @@ -55,24 +58,6 @@ const Empty = () => { ); }; -const useAcceptFriend = () => { - const queryClient = useQueryClient(); - const { friendService } = useServiceContext(); - return useMutation({ - mutationFn: (friendId: FriendId) => friendService.acceptFriend({ friendId }), - onSuccess: () => queryClient.invalidateQueries(), - }); -}; - -const useDeclineFriend = () => { - const queryClient = useQueryClient(); - const { friendService } = useServiceContext(); - return useMutation({ - mutationFn: (friendId: FriendId) => friendService.declineFriend({ friendId }), - onSuccess: () => queryClient.invalidateQueries(), - }); -}; - const styles = StyleSheet.create({ item: { height: 40, diff --git a/apps/friends-react-native/src/app/screens/MainScreen/ManageFriendsDrawerContent/index.tsx b/apps/friends-react-native/src/app/screens/MainScreen/ManageFriendsDrawerContent/index.tsx index 9f75d37..44d4a28 100644 --- a/apps/friends-react-native/src/app/screens/MainScreen/ManageFriendsDrawerContent/index.tsx +++ b/apps/friends-react-native/src/app/screens/MainScreen/ManageFriendsDrawerContent/index.tsx @@ -67,7 +67,7 @@ export const ManageFriendsDrawerContent = ({ onClose }: Props) => { dispatch({ type: 'setAddFriendModalOpen', isOpen: true })} + onPress={() => dispatch({ type: 'setRequestFriendModalOpen', isOpen: true })} > 친구 추가하기 diff --git a/apps/friends-react-native/src/app/screens/MainScreen/RequestFriendsBottomSheetContent/RequestFriendsMethodList/index.tsx b/apps/friends-react-native/src/app/screens/MainScreen/RequestFriendsBottomSheetContent/RequestFriendsMethodList/index.tsx new file mode 100644 index 0000000..7387b9d --- /dev/null +++ b/apps/friends-react-native/src/app/screens/MainScreen/RequestFriendsBottomSheetContent/RequestFriendsMethodList/index.tsx @@ -0,0 +1,80 @@ +import { StyleSheet, View } from 'react-native'; + +import { Typography } from '../../../../components/Typography'; +import { TouchableOpacity } from 'react-native-gesture-handler'; +import { UserHashtagIcon } from '../../../../components/Icons/UserHashtagIcon'; +import { useThemeContext } from '../../../../contexts/ThemeContext'; +import { KakaotalkIcon } from '../../../../components/Icons/Kakaotalk'; +import { RequestFriendModalStep, useMainScreenContext } from '../..'; +import { useRequestFriendToken } from '../../../../queries/useRequestFriendToken'; +import { useServiceContext } from '../../../../contexts/ServiceContext'; + +export const RequestFriendsMethodList = () => { + const { dispatch } = useMainScreenContext(); + const { nativeEventService } = useServiceContext(); + const { data } = useRequestFriendToken(); + const iconColor = useThemeContext((theme) => theme.color.text.default); + + const setRequestFriendModalStep = (step: RequestFriendModalStep) => + dispatch({ + type: 'setRequestFriendModalStep', + requestFriendModalStep: step, + }); + + const requestFriendWithKakao = () => { + const parameters = { + requestToken: data!.requestToken, + }; + + nativeEventService.sendEventToNative({ + type: 'add-friend-kakao', + parameters, + }); + + dispatch({ + type: 'setRequestFriendModalOpen', + isOpen: false, + }); + }; + + return ( + <> + + + + 카카오톡으로 친구 초대 + + + + setRequestFriendModalStep('REQUEST_WITH_NICKNAME')}> + + 닉네임으로 친구 초대 + + + + ); +}; + +const styles = StyleSheet.create({ + sheetContent: { paddingBottom: 20 }, + sheetItem: { + height: 50, + paddingVertical: 10, + display: 'flex', + flexDirection: 'row', + gap: 25, + alignItems: 'center', + }, +}); diff --git a/apps/friends-react-native/src/app/screens/MainScreen/RequestFriendsBottomSheetContent/RequestFriendsWithNickname/index.tsx b/apps/friends-react-native/src/app/screens/MainScreen/RequestFriendsBottomSheetContent/RequestFriendsWithNickname/index.tsx new file mode 100644 index 0000000..4d61ab9 --- /dev/null +++ b/apps/friends-react-native/src/app/screens/MainScreen/RequestFriendsBottomSheetContent/RequestFriendsWithNickname/index.tsx @@ -0,0 +1,90 @@ +import { Alert } from 'react-native'; +import { StyleSheet, View } from 'react-native'; + +import { get } from '../../../../../utils/get'; +import { BottomSheet } from '../../../../components/BottomSheet'; +import { WarningIcon } from '../../../../components/Icons/WarningIcon'; +import { Input } from '../../../../components/Input'; +import { Typography } from '../../../../components/Typography'; +import { COLORS } from '../../../../styles/colors'; +import { useMainScreenContext, useRequestFriend } from '../..'; +import { useServiceContext } from '../../../../contexts/ServiceContext'; +import { useThemeContext } from '../../../../contexts/ThemeContext'; + +export const RequestFriendsWithNickname = () => { + const { requestFriendModalNickname, dispatch } = useMainScreenContext(); + const { friendService } = useServiceContext(); + const guideEnabledColor = useThemeContext((data) => data.color.text.guide); + const { mutate: request } = useRequestFriend(); + + const isValid = friendService.isValidNicknameTag(requestFriendModalNickname); + const guideMessageState = requestFriendModalNickname === '' ? 'disabled' : isValid ? 'hidden' : 'enabled'; + + const closeAddFriendModal = () => dispatch({ type: 'setRequestFriendModalOpen', isOpen: false }); + + return ( + + + request(requestFriendModalNickname, { + onSuccess: () => { + Alert.alert('친구에게 요청을 보냈습니다.'); + closeAddFriendModal(); + }, + onError: (err) => { + const displayMessage = get(err, ['displayMessage']); + Alert.alert(displayMessage ? `${displayMessage}` : '오류가 발생했습니다.'); + }, + }), + disabled: !isValid, + }} + /> + + 추가하고 싶은 친구의 닉네임 + + dispatch({ type: 'setRequestFriendModalNickname', nickname: e })} + placeholder="예) 홍길동#1234" + /> + + {guideMessageState !== 'hidden' && + (() => { + const color = { enabled: guideEnabledColor, disabled: COLORS.gray40 }[guideMessageState]; + return ( + <> + + + 닉네임 전체를 입력하세요 + + + ); + })()} + + + ); +}; + +const styles = StyleSheet.create({ + questionIconButton: { flexDirection: 'row', alignItems: 'center', gap: 6 }, + questionIcon: { color: COLORS.gray30 }, + modalContent: { paddingBottom: 30 }, + inputDescription: { marginTop: 30, fontSize: 14 }, + input: { marginTop: 15 }, + guide: { + marginTop: 7, + display: 'flex', + flexDirection: 'row', + gap: 1, + alignItems: 'center', + height: 12, + }, + guideText: { fontSize: 12 }, + hamburgerWrapper: { position: 'relative' }, + hamburgerNotificationDot: { position: 'absolute', top: 5, right: -1 }, +}); diff --git a/apps/friends-react-native/src/app/screens/MainScreen/RequestFriendsBottomSheetContent/index.tsx b/apps/friends-react-native/src/app/screens/MainScreen/RequestFriendsBottomSheetContent/index.tsx new file mode 100644 index 0000000..828c0c8 --- /dev/null +++ b/apps/friends-react-native/src/app/screens/MainScreen/RequestFriendsBottomSheetContent/index.tsx @@ -0,0 +1,16 @@ +import { useMainScreenContext } from '..'; +import { BottomSheet } from '../../../components/BottomSheet'; +import { RequestFriendsMethodList } from './RequestFriendsMethodList'; +import { RequestFriendsWithNickname } from './RequestFriendsWithNickname'; + +export const RequestFriendsBottomSheetContent = () => { + const { isRequestFriendModalOpen, requestFriendModalStep, dispatch } = useMainScreenContext(); + + const closeAddFriendModal = () => dispatch({ type: 'setRequestFriendModalOpen', isOpen: false }); + + return ( + + {requestFriendModalStep === 'METHOD_LIST' ? : } + + ); +}; diff --git a/apps/friends-react-native/src/app/screens/MainScreen/index.tsx b/apps/friends-react-native/src/app/screens/MainScreen/index.tsx index 28f0672..bceaa56 100644 --- a/apps/friends-react-native/src/app/screens/MainScreen/index.tsx +++ b/apps/friends-react-native/src/app/screens/MainScreen/index.tsx @@ -1,44 +1,44 @@ import { createDrawerNavigator, DrawerContentComponentProps, DrawerHeaderProps } from '@react-navigation/drawer'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { createContext, Dispatch, useContext, useEffect, useMemo, useReducer } from 'react'; -import { Alert, TouchableOpacity } from 'react-native'; +import { TouchableOpacity } from 'react-native'; import { StyleSheet, View } from 'react-native'; import { CourseBook } from '../../../entities/courseBook'; import { ClientFeature } from '../../../entities/feature'; import { FriendId } from '../../../entities/friend'; import { Nickname } from '../../../entities/user'; -import { get } from '../../../utils/get'; import { AppBar } from '../../components/Appbar'; -import { BottomSheet } from '../../components/BottomSheet'; import { HamburgerIcon } from '../../components/Icons/HamburgerIcon'; import { QuestionIcon } from '../../components/Icons/QuestionIcon'; import { UserPlusIcon } from '../../components/Icons/UserPlusIcon'; -import { WarningIcon } from '../../components/Icons/WarningIcon'; -import { Input } from '../../components/Input'; import { NotificationDot } from '../../components/NotificationDot'; -import { Typography } from '../../components/Typography'; import { useFeatureContext } from '../../contexts/FeatureContext'; import { useServiceContext } from '../../contexts/ServiceContext'; import { useThemeContext } from '../../contexts/ThemeContext'; import { useFriendCourseBooks } from '../../queries/useFriendCourseBooks'; -import { useFriends } from '../../queries/useFriends'; +import { useAcceptFriend, useFriends } from '../../queries/useFriends'; import { COLORS } from '../../styles/colors'; import { FriendTimetable } from './FriendTimetable'; import { ManageFriendsDrawerContent } from './ManageFriendsDrawerContent'; +import { RequestFriendsBottomSheetContent } from './RequestFriendsBottomSheetContent'; + +export type RequestFriendModalStep = 'METHOD_LIST' | 'REQUEST_WITH_NICKNAME'; type MainScreenState = { selectedFriendId: FriendId | undefined; selectedCourseBook: CourseBook | undefined; - isAddFriendModalOpen: boolean; - addFriendModalNickname: string; + isRequestFriendModalOpen: boolean; + requestFriendModalStep: RequestFriendModalStep; + requestFriendModalNickname: string; isGuideModalOpen: boolean; }; type MainScreenAction = | { type: 'setFriend'; friendId: FriendId | undefined } | { type: 'setCourseBook'; courseBook: CourseBook } - | { type: 'setAddFriendModalOpen'; isOpen: boolean } - | { type: 'setAddFriendModalNickname'; nickname: string } + | { type: 'setRequestFriendModalOpen'; isOpen: boolean } + | { type: 'setRequestFriendModalStep'; requestFriendModalStep: RequestFriendModalStep } + | { type: 'setRequestFriendModalNickname'; nickname: string } | { type: 'setGuideModalOpen'; isOpen: boolean }; type MainScreenContext = MainScreenState & { dispatch: Dispatch }; const mainScreenReducer = (state: MainScreenState, action: MainScreenAction): MainScreenState => { @@ -47,15 +47,22 @@ const mainScreenReducer = (state: MainScreenState, action: MainScreenAction): Ma return { ...state, selectedFriendId: action.friendId, selectedCourseBook: undefined }; case 'setCourseBook': return { ...state, selectedCourseBook: action.courseBook }; - case 'setAddFriendModalOpen': + case 'setRequestFriendModalOpen': return action.isOpen - ? { ...state, isAddFriendModalOpen: true } - : { ...state, isAddFriendModalOpen: false, addFriendModalNickname: '' }; - case 'setAddFriendModalNickname': - if (!state.isAddFriendModalOpen) throw new Error(); - return { ...state, addFriendModalNickname: action.nickname }; + ? { ...state, isRequestFriendModalOpen: true } + : { + ...state, + isRequestFriendModalOpen: false, + requestFriendModalNickname: '', + requestFriendModalStep: 'METHOD_LIST', + }; + case 'setRequestFriendModalNickname': + if (!state.isRequestFriendModalOpen) throw new Error(); + return { ...state, requestFriendModalNickname: action.nickname }; case 'setGuideModalOpen': return { ...state, isGuideModalOpen: action.isOpen }; + case 'setRequestFriendModalStep': + return { ...state, requestFriendModalStep: action.requestFriendModalStep }; } }; const mainScreenContext = createContext(null); @@ -70,14 +77,19 @@ export const MainScreen = () => { const [state, dispatch] = useReducer(mainScreenReducer, { selectedFriendId: undefined, selectedCourseBook: undefined, - isAddFriendModalOpen: false, - addFriendModalNickname: '', + isRequestFriendModalOpen: false, + requestFriendModalStep: 'METHOD_LIST', + requestFriendModalNickname: '', isGuideModalOpen: false, }); + const { nativeEventService } = useServiceContext(); const { clientFeatures } = useFeatureContext(); const { data: friends } = useFriends({ state: 'ACTIVE' }); + const { mutate: acceptFriend } = useAcceptFriend(); + + const eventEmitter = nativeEventService.getEventEmitter(); useEffect(() => { if (!clientFeatures.includes(ClientFeature.ASYNC_STORAGE)) return; @@ -96,6 +108,31 @@ export const MainScreen = () => { .catch(() => null); }, [state.selectedFriendId, clientFeatures, friends]); + useEffect(() => { + const parameters = { eventType: 'add-friend-kakao' }; + + const listener = eventEmitter.addListener('add-friend-kakao', (event) => { + acceptFriend({ + type: 'KAKAO', + requestToken: event.requstToken, + }); + }); + + nativeEventService.sendEventToNative({ + type: 'register', + parameters, + }); + + return () => { + listener.remove(); + + nativeEventService.sendEventToNative({ + type: 'deregister', + parameters, + }); + }; + }, [eventEmitter, nativeEventService, acceptFriend]); + const backgroundColor = useThemeContext((data) => data.color.bg.default); const selectedFriendIdWithDefault = state.selectedFriendId ?? friends?.at(0)?.friendId; const { data: courseBooks } = useFriendCourseBooks(selectedFriendIdWithDefault); @@ -107,16 +144,18 @@ export const MainScreen = () => { () => ({ selectedFriendId: selectedFriendIdWithDefault, selectedCourseBook: selectedCourseBookWithDefault, - isAddFriendModalOpen: state.isAddFriendModalOpen, - addFriendModalNickname: state.addFriendModalNickname, + isRequestFriendModalOpen: state.isRequestFriendModalOpen, + requestFriendModalStep: state.requestFriendModalStep, + requestFriendModalNickname: state.requestFriendModalNickname, isGuideModalOpen: state.isGuideModalOpen, dispatch, }), [ selectedFriendIdWithDefault, selectedCourseBookWithDefault, - state.isAddFriendModalOpen, - state.addFriendModalNickname, + state.isRequestFriendModalOpen, + state.requestFriendModalStep, + state.requestFriendModalNickname, state.isGuideModalOpen, ], )} @@ -132,18 +171,12 @@ export const MainScreen = () => { }; const Header = ({ navigation }: DrawerHeaderProps) => { - const { addFriendModalNickname, isAddFriendModalOpen, dispatch } = useMainScreenContext(); - const { friendService } = useServiceContext(); - const { mutate: request } = useRequestFriend(); - const guideEnabledColor = useThemeContext((data) => data.color.text.guide); + const { dispatch } = useMainScreenContext(); const { data: requestedFriends } = useFriends({ state: 'REQUESTED' }); const isRequestedFriendExist = requestedFriends && requestedFriends.length !== 0; - const isValid = friendService.isValidNicknameTag(addFriendModalNickname); - const guideMessageState = addFriendModalNickname === '' ? 'disabled' : isValid ? 'hidden' : 'enabled'; - const openAddFriendModal = () => dispatch({ type: 'setAddFriendModalOpen', isOpen: true }); - const closeAddFriendModal = () => dispatch({ type: 'setAddFriendModalOpen', isOpen: false }); + const openAddFriendModal = () => dispatch({ type: 'setRequestFriendModalOpen', isOpen: true }); const openGuideModal = () => dispatch({ type: 'setGuideModalOpen', isOpen: true }); return ( @@ -170,52 +203,7 @@ const Header = ({ navigation }: DrawerHeaderProps) => { } /> - - - - request(addFriendModalNickname, { - onSuccess: () => { - Alert.alert('친구에게 요청을 보냈습니다.'); - closeAddFriendModal(); - }, - onError: (err) => { - const displayMessage = get(err, ['displayMessage']); - Alert.alert(displayMessage ? `${displayMessage}` : '오류가 발생했습니다.'); - }, - }), - disabled: !isValid, - }} - /> - - 추가하고 싶은 친구의 닉네임 - - dispatch({ type: 'setAddFriendModalNickname', nickname: e })} - placeholder="예) 홍길동#1234" - /> - - {guideMessageState !== 'hidden' && - (() => { - const color = { enabled: guideEnabledColor, disabled: COLORS.gray40 }[guideMessageState]; - return ( - <> - - - 닉네임 전체를 입력하세요 - - - ); - })()} - - - + ); }; @@ -224,7 +212,7 @@ const DrawerContent = ({ navigation }: DrawerContentComponentProps) => { return navigation.closeDrawer()} />; }; -const useRequestFriend = () => { +export const useRequestFriend = () => { const { friendService } = useServiceContext(); const queryClient = useQueryClient(); diff --git a/apps/friends-react-native/src/infrastructures/createFriendRepository.ts b/apps/friends-react-native/src/infrastructures/createFriendRepository.ts index 6f8baba..2bedf8f 100644 --- a/apps/friends-react-native/src/infrastructures/createFriendRepository.ts +++ b/apps/friends-react-native/src/infrastructures/createFriendRepository.ts @@ -9,6 +9,7 @@ export const createFriendRepository = (apiClient: ApiClient): FriendRepository = apiClient.get>>(`/v1/friends?state=${state}`), requestFriend: ({ nickname }) => apiClient.post('/v1/friends', { nickname }), acceptFriend: ({ friendId }) => apiClient.post(`/v1/friends/${friendId}/accept`), + acceptFriendWithKakao: ({ requestToken }) => apiClient.post(`/v1/friends/accept-link/${requestToken}`), declineFriend: ({ friendId }) => apiClient.post(`/v1/friends/${friendId}/decline`), deleteFriend: ({ friendId }) => apiClient.delete(`/v1/friends/${friendId}`), getFriendPrimaryTable: ({ friendId, semester, year }) => @@ -16,5 +17,6 @@ export const createFriendRepository = (apiClient: ApiClient): FriendRepository = getFriendCourseBooks: ({ friendId }) => apiClient.get(`/v1/friends/${friendId}/coursebooks`), patchFriendDisplayName: ({ friendId, displayName }) => apiClient.patch(`/v1/friends/${friendId}/display-name`, { displayName }), + generateToken: () => apiClient.get<{ requestToken: string }>('/v1/friends/generate-link'), }; }; diff --git a/apps/friends-react-native/src/infrastructures/createFriendService.ts b/apps/friends-react-native/src/infrastructures/createFriendService.ts index b3af6ff..e1f6a6e 100644 --- a/apps/friends-react-native/src/infrastructures/createFriendService.ts +++ b/apps/friends-react-native/src/infrastructures/createFriendService.ts @@ -11,13 +11,17 @@ export const createFriendService = ({ friendRepository .listFriends(req) .then((res) => res.content.map((c) => ({ friendId: c.id, ...c.nickname, displayName: c.displayName }))), - acceptFriend: (req) => friendRepository.acceptFriend(req), + acceptFriend: (req) => + req.type === 'NICKNAME' + ? friendRepository.acceptFriend({ friendId: req.friendId }) + : friendRepository.acceptFriendWithKakao({ requestToken: req.requestToken }), declineFriend: (req) => friendRepository.declineFriend(req), deleteFriend: (req) => friendRepository.deleteFriend(req), requestFriend: (req) => friendRepository.requestFriend(req), getFriendPrimaryTable: (req) => friendRepository.getFriendPrimaryTable(req), getFriendCourseBooks: (req) => friendRepository.getFriendCourseBooks(req), patchFriendDisplayName: (req) => friendRepository.patchFriendDisplayName(req), + generateToken: () => friendRepository.generateToken(), formatNickname: (req, options = { type: 'default' }) => { const displayName = req.displayName; diff --git a/apps/friends-react-native/src/infrastructures/createNativeEventService.ts b/apps/friends-react-native/src/infrastructures/createNativeEventService.ts new file mode 100644 index 0000000..2ec1542 --- /dev/null +++ b/apps/friends-react-native/src/infrastructures/createNativeEventService.ts @@ -0,0 +1,13 @@ +import { NativeEventEmitter, NativeModules } from 'react-native'; +import { NativeEvent, NativeEventService } from '../usecases/nativeEventService'; + +export const createNativeEventService = (): NativeEventService => { + const eventEmitter = new NativeEventEmitter(NativeModules.RNEventEmitter); + + return { + getEventEmitter: () => eventEmitter, + sendEventToNative: (event: NativeEvent) => { + NativeModules.RNEventEmitter.sendEventToNative(event.type, event.parameters); + }, + }; +}; diff --git a/apps/friends-react-native/src/main.tsx b/apps/friends-react-native/src/main.tsx index fe4d550..ca8beb0 100644 --- a/apps/friends-react-native/src/main.tsx +++ b/apps/friends-react-native/src/main.tsx @@ -18,6 +18,7 @@ import { createFetchClient } from './infrastructures/createFetchClient'; import { createFriendRepository } from './infrastructures/createFriendRepository'; import { createFriendService } from './infrastructures/createFriendService'; import { createTimetableViewService } from './infrastructures/createTimetableViewService'; +import { createNativeEventService } from './infrastructures/createNativeEventService'; type ExternalProps = { 'x-access-token': string; @@ -48,10 +49,11 @@ export const Main = ({ const colorService = createColorService({ repositories: [createColorRepository({ clients: [fetchClient] })] }); const friendService = createFriendService({ repositories: [friendRepository] }); const courseBookService = createCourseBookService(); + const nativeEventService = createNativeEventService(); const serviceValue = useMemo( - () => ({ timetableViewService, colorService, friendService, courseBookService, assetService }), - [timetableViewService, colorService, friendService, courseBookService, assetService], + () => ({ timetableViewService, colorService, friendService, courseBookService, assetService, nativeEventService }), + [timetableViewService, colorService, friendService, courseBookService, assetService, nativeEventService], ); const themeValue = useMemo(() => getThemeValues(theme), [theme]); diff --git a/apps/friends-react-native/src/repositories/friendRepository.ts b/apps/friends-react-native/src/repositories/friendRepository.ts index b2c52e2..75a654b 100644 --- a/apps/friends-react-native/src/repositories/friendRepository.ts +++ b/apps/friends-react-native/src/repositories/friendRepository.ts @@ -23,6 +23,8 @@ export type FriendRepository = { acceptFriend: (req: { friendId: FriendId }) => Promise; + acceptFriendWithKakao: (req: { requestToken: string }) => Promise; + declineFriend: (req: { friendId: FriendId }) => Promise; deleteFriend: (req: { friendId: FriendId }) => Promise; @@ -32,4 +34,6 @@ export type FriendRepository = { getFriendCourseBooks: (req: { friendId: FriendId }) => Promise; patchFriendDisplayName: (req: { friendId: FriendId; displayName: DisplayName }) => Promise; + + generateToken: () => Promise<{ requestToken: string }>; }; diff --git a/apps/friends-react-native/src/types/native.d.ts b/apps/friends-react-native/src/types/native.d.ts new file mode 100644 index 0000000..9c83fdb --- /dev/null +++ b/apps/friends-react-native/src/types/native.d.ts @@ -0,0 +1,11 @@ +import { NativeModule } from 'react-native'; + +declare module 'react-native' { + interface NativeModulesStatic { + RNEventEmitter: NativeModule & { + sendEventToNative: (name: string, parameters?: Record) => void; + }; + } +} + +export {}; diff --git a/apps/friends-react-native/src/usecases/friendService.ts b/apps/friends-react-native/src/usecases/friendService.ts index 7bd2c6b..3ece1a6 100644 --- a/apps/friends-react-native/src/usecases/friendService.ts +++ b/apps/friends-react-native/src/usecases/friendService.ts @@ -10,12 +10,15 @@ export type FriendService = { state: 'ACTIVE' | 'REQUESTED' | 'REQUESTING'; }) => Promise<{ friendId: FriendId; nickname: Nickname; tag: NicknameTag; displayName?: DisplayName }[]>; requestFriend: (req: { nickname: Nickname }) => Promise; - acceptFriend: (req: { friendId: FriendId }) => Promise; + acceptFriend: ( + req: { type: 'NICKNAME'; friendId: FriendId } | { type: 'KAKAO'; requestToken: string }, + ) => Promise; declineFriend: (req: { friendId: FriendId }) => Promise; deleteFriend: (req: { friendId: FriendId }) => Promise; getFriendPrimaryTable: (req: { friendId: FriendId; semester: Semester; year: Year }) => Promise; getFriendCourseBooks: (req: { friendId: FriendId }) => Promise; patchFriendDisplayName: (req: { friendId: FriendId; displayName: DisplayName }) => Promise; + generateToken: () => Promise<{ requestToken: string }>; formatNickname: ( req: { nickname: Nickname; tag: NicknameTag; displayName?: DisplayName }, diff --git a/apps/friends-react-native/src/usecases/nativeEventService.ts b/apps/friends-react-native/src/usecases/nativeEventService.ts new file mode 100644 index 0000000..163414e --- /dev/null +++ b/apps/friends-react-native/src/usecases/nativeEventService.ts @@ -0,0 +1,10 @@ +import { NativeEventEmitter } from 'react-native'; + +export type NativeEvent = + | { type: 'register' | 'deregister'; parameters: { eventType: string } } + | { type: 'add-friend-kakao'; parameters: { requestToken: string } }; + +export type NativeEventService = { + getEventEmitter: () => NativeEventEmitter; + sendEventToNative: (event: NativeEvent) => void; +};