diff --git a/.tours/--.tour b/.tours/--.tour new file mode 100644 index 0000000..61d2f00 --- /dev/null +++ b/.tours/--.tour @@ -0,0 +1,26 @@ +{ + "$schema": "https://aka.ms/codetour-schema", + "title": "로그인 토큰 처리", + "steps": [ + { + "file": "app/screens/index.tsx", + "description": "### 로그인 버튼 화면", + "line": 27 + }, + { + "file": "app/assets/util/login.ts", + "description": "### 소셜로그인 로직", + "line": 8 + }, + { + "file": "app/hooks/login/login.ts", + "description": "### 로그인 훅", + "line": 7 + }, + { + "file": "app/api/login.ts", + "description": "### 리프레쉬토큰API", + "line": 35 + } + ] +} \ No newline at end of file diff --git a/App.tsx b/App.tsx index ecdcc9b..14b914c 100644 --- a/App.tsx +++ b/App.tsx @@ -10,12 +10,12 @@ import { navigationRef } from './RootNavigation'; import SimpleSnackbarUI from '~/components/common/toast'; import { useApiError } from '~/hooks/useApiError'; import { AppStateComponent } from '~/observers/app-state'; -// import { AxiosConfig } from './axiosConfig'; +import { AxiosConfig } from './axiosConfig'; import { EventProvider } from 'react-native-outside-press'; import 'react-native-gesture-handler'; import axios, { AxiosResponse } from 'axios'; -import { getStorage } from '~/assets/util/storage'; -import { SERVER_URL, TOKEN_STORAGE_KEY } from '~/assets/util/constants'; +import { getStorage, get } from '~/assets/util/storage'; +import { SERVER_URL } from '~/assets/util/constants'; import { CustomResponse } from 'types/modules'; import * as encoding from 'text-encoding'; import { WebSocketConnector } from '~/components/chat/web-socket-connector'; @@ -33,28 +33,6 @@ if (__DEV__) { ); } -axios.defaults.baseURL = SERVER_URL; - -axios.interceptors.request.use( - async (config) => { - const token = await getStorage(TOKEN_STORAGE_KEY); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; - }, - (error) => { - return Promise.reject(error); - }, -); - -axios.interceptors.response.use( - (response: AxiosResponse>) => { - console.log('@@ AXIOS RESPONSE 123', response); - return response.data.data; - }, -); - const Stack = createNativeStackNavigator(); function RootNavigator() { @@ -75,6 +53,7 @@ export default function App() { + diff --git a/app/api/login.ts b/app/api/login.ts index 378c871..1bb4234 100644 --- a/app/api/login.ts +++ b/app/api/login.ts @@ -1,5 +1,10 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; import axios from 'axios'; +import AlertPopup from 'react-native-global-components/components/AlertPopup/AlertPopup'; import { Envelope, SocialLoginRoute } from 'types/common'; +import { navigationRef } from '~/../RootNavigation'; +import { NT_REFRESH_TOKEN } from '~/assets/util/constants'; +import { clearStorage, get } from '~/assets/util/storage'; interface TokenParams { id_token: string; @@ -16,8 +21,55 @@ export const postLogin = async ( route: SocialLoginRoute, params: TokenParams, ) => { - console.log(route, params); + console.log('[postLogin]', route, params); // TODO: 사실 이 부분은 api가 통합되어야 할 듯 => 수정될 것 같음 return axios.post(`/login/${route}`, params); }; + +// WIP 에러코드랑 토큰 보내느 형식 다시 맞춰야할듯 +let cached: any = null; +let lastCall = 0; +const maxAge = 1000; // 캐시 유지 기간 (밀리초) + +export const refreshTokenAPI = async () => { + // console.group('refreshTokenAPI 시작 =>'); + // const now = Date.now(); + + // if (cached !== null && now - lastCall < maxAge) { + // return cached; // 캐시된 결과 반환 + // } + + // lastCall = now; // 호출 시간 업데이트 + + try { + const refresh_token = await AsyncStorage.getItem(NT_REFRESH_TOKEN); + console.log('refresh_token =>', refresh_token); + + const response = await axios.post( + `/user/reToken`, + {}, // 요청 본문이 필요 없다면 빈 객체를 전달합니다. + { + headers: { + Authorization: `Bearer ${refresh_token}`, + }, + }, + ); + console.log('response =>', response); + + cached = response.data; // 응답 캐시 + + return cached; + } catch (error) { + cached = null; // 오류 발생 시 캐시 초기화 + + console.log('!!error', error); + // 리프레쉬 토큰도 만료되었다면? + // 리프레시 토큰 만료 처리 로직 + if (error) { + // 리프레시 토큰도 만료됨으로써 로그인페이지로 리다이렉트 처리해야함 + // AsyncStorage.clear(); + navigationRef.current.navigate('RootScreen'); + } + } +}; diff --git a/app/api/user.ts b/app/api/user.ts index e9d69a8..f6c4819 100644 --- a/app/api/user.ts +++ b/app/api/user.ts @@ -1,8 +1,37 @@ import axios from 'axios'; -import { userInfoType } from '~/types/user'; +import { loadTokens } from '~/hooks/login/login'; +import { TOKEN_ERROR_STATUS, userInfoType } from '~/types/user'; export const getUserInfoAPI = async () => { - return await axios.get('/user'); + const { accessToken } = await loadTokens(); // 비동기적으로 토큰 로드 + + return await axios + .get('/user', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + .then((res) => { + console.log('res', res); + return res; + }) + .catch((err) => { + // console.error('err', err); + // // 토큰 관련 에러 + // if (err.status == 401) { + // switch (err.message) { + // case TOKEN_ERROR_STATUS.ACCESS_TOKEN_EXPIRED: + // // 액세스 토큰 만료 시 + // // user/reToken API에 리프레쉬 토큰을 담아 호출하여, 토큰 재발급 후 다시 요청하게끔. + // ''; + // case TOKEN_ERROR_STATUS.REFRESH_TOKEN_EXPIRED: + // // 리프레쉬 토큰 만료 시 + // // 액세스와 리프레쉬 토큰 둘 다 만료임으로, 로그인 화면으로 리다이렉트 시킨다. + // ''; + // } + // } + // console.log('err', err); + }); }; export const patchUserInfoAPI = (form: FormData) => diff --git a/app/assets/util/constants.ts b/app/assets/util/constants.ts index fb9a57f..0cdd41e 100644 --- a/app/assets/util/constants.ts +++ b/app/assets/util/constants.ts @@ -12,8 +12,19 @@ export const sortOptions: SortType[] = [ { nm: '가까운순', cd: 'distance' }, ]; -export const TOKEN_STORAGE_KEY = 'NT-AUTH-TOKEN'; +export const NT_ACCESS_TOKEN = 'NT-ACCESS-TOKEN'; +export const NT_REFRESH_TOKEN = 'NT-REFRESH-TOKEN'; const BASE_URL = 'nthing.kkookkss.synology.me'; export const SERVER_URL = `https://${BASE_URL}`; export const WEBSOCKET_SERVER_URL = `wss://${BASE_URL}`; + +export const naverLoginKeys = { + consumerKey: 'vnH89uX9Nczv8vOeXfQw', // 이거 필요한건가? + consumerSecret: 'TtWl5HamP7', // 얘도 필요한건가? + appName: 'nThing', + serviceUrlScheme: 'naverlogin', // only for iOS +}; + +export const googleWebClientId = + '141023294009-g5k49bh6cmk0re3c94mnu9esi4ep3gcc.apps.googleusercontent.com'; diff --git a/app/assets/util/login.ts b/app/assets/util/login.ts new file mode 100644 index 0000000..343b995 --- /dev/null +++ b/app/assets/util/login.ts @@ -0,0 +1,83 @@ +import NaverLogin, { NaverLoginRequest } from '@react-native-seoul/naver-login'; +import { postLogin } from '~/api/login'; +import { SocialLoginRoute } from '~/types/common'; +import * as KakaoLogin from '@react-native-seoul/kakao-login'; +import { GoogleSignin } from '@react-native-google-signin/google-signin'; +import { + googleWebClientId, + naverLoginKeys, + NT_ACCESS_TOKEN, + NT_REFRESH_TOKEN, +} from './constants'; +import auth from '@react-native-firebase/auth'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { setToken } from '~/hooks/login/login'; + +interface loginResponseType { + data: { message: string; access_token: string; refresh_token: string }; + message: null; + status: number; + code: null; +} + +export const getServiceToken = async ( + social: SocialLoginRoute, + idToken: string, +) => { + try { + console.log('getServiceToken ::', social, idToken); + const res: loginResponseType = await postLogin(social, { + id_token: idToken, + }); + + console.log('응답', res); + return res; + } catch (error) { + console.error('Token retrieval failed:', error); + throw error; + } +}; + +export const naverLogin = async () => { + const NAVER_LOGIN_KEY: NaverLoginRequest = naverLoginKeys; + try { + const { successResponse } = await NaverLogin.login(NAVER_LOGIN_KEY); + console.log('네이버 토큰', successResponse?.accessToken); + } catch (e) { + console.warn(e); + } +}; + +export const kakaoLogin = async () => { + try { + const { accessToken } = await KakaoLogin.login(); + // console.log('카카오 토큰', accessToken); + + const token = await getServiceToken('kakao', accessToken); + console.log('getServiceToken카카오 토큰', token); + setToken(token); + + return token; + } catch (e) { + console.log('kakaoLogin Error', e); + } +}; + +export const googleLogin = async () => { + GoogleSignin.configure({ + webClientId: googleWebClientId, + }); + try { + const { idToken } = await GoogleSignin.signIn(); + // console.log('구글 토큰', idToken); + const googleCredential = auth.GoogleAuthProvider.credential(idToken); + const firebaseToken = await auth() + .signInWithCredential(googleCredential) + .then(({ user }) => user.getIdToken()); + // console.log('새로 받은 토큰', firebaseToken); + const token = await getServiceToken('google', firebaseToken); + return token; + } catch (e) { + console.warn(e); + } +}; diff --git a/app/assets/util/storage.ts b/app/assets/util/storage.ts index d087d74..c6a91af 100644 --- a/app/assets/util/storage.ts +++ b/app/assets/util/storage.ts @@ -1,4 +1,5 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; +import { NT_ACCESS_TOKEN } from './constants'; export const getStorage = async (key: string) => { const result = await AsyncStorage.getItem(key); @@ -14,3 +15,15 @@ export const removeStorage = async (key: string) => { }; export const clearStorage = AsyncStorage.clear(); +export const get = async () => { + try { + const savedToken = await AsyncStorage.getItem(NT_ACCESS_TOKEN); + const token = savedToken ? JSON.parse(savedToken) : null; // 저장된 값이 없을 경우 null 처리 + console.log('get token', token); + + return token; + } catch (error) { + console.log('Error getting token:', error); + return null; // 에러 발생 시 null 반환 + } +}; diff --git a/app/assets/util/web-socket.ts b/app/assets/util/web-socket.ts index 07b13ae..fd3a8b3 100644 --- a/app/assets/util/web-socket.ts +++ b/app/assets/util/web-socket.ts @@ -1,5 +1,5 @@ import { Frame, messageCallbackType, Stomp, StompConfig } from '@stomp/stompjs'; -import { TOKEN_STORAGE_KEY, WEBSOCKET_SERVER_URL } from 'assets/util/constants'; +import { NT_ACCESS_TOKEN, WEBSOCKET_SERVER_URL } from 'assets/util/constants'; import { getStorage } from 'assets/util/storage'; import { ChatMessage, WebsocketMessageType } from 'types/chat'; @@ -12,7 +12,7 @@ export const connect = async ( roomIds: number[], callback: messageCallbackType, ) => { - const token = await getStorage(TOKEN_STORAGE_KEY); + const token = await getStorage(NT_ACCESS_TOKEN); // console.log('웹소켓에 보낼 token ' + token); stompClient.connect({ Authorization: `Bearer ${token}` }, (frame) => { console.log('hi connected', frame); @@ -23,7 +23,7 @@ export const connect = async ( }; export const send = async (roomId: number, message: ChatMessage) => { - const token = await getStorage(TOKEN_STORAGE_KEY); + const token = await getStorage(NT_ACCESS_TOKEN); stompClient.send( `/send-to/room/${roomId}`, { Authorization: `Bearer ${token}` }, diff --git a/app/hooks/chatting/chat-rooms.tsx b/app/hooks/chatting/chat-rooms.tsx index f042f76..cce1a99 100644 --- a/app/hooks/chatting/chat-rooms.tsx +++ b/app/hooks/chatting/chat-rooms.tsx @@ -3,9 +3,12 @@ import { getChatRoomsAPI } from 'api/chatting'; import { chatRoomsKeys } from 'key/chatting'; export function useChatRooms() { + const test = false; + const chatRooms = useQuery({ queryKey: chatRoomsKeys.info(), queryFn: getChatRoomsAPI, + enabled: test, }); return chatRooms; diff --git a/app/hooks/login/login.ts b/app/hooks/login/login.ts new file mode 100644 index 0000000..f4497ba --- /dev/null +++ b/app/hooks/login/login.ts @@ -0,0 +1,82 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { useRecoilState } from 'recoil'; +import { navigationRef } from '~/../RootNavigation'; +import { NT_ACCESS_TOKEN, NT_REFRESH_TOKEN } from '~/assets/util/constants'; +import { TokenState } from '~/state/user'; + +// 토큰 저장 +export const saveTokens = async (accessToken: string, refreshToken: string) => { + console.log('saveTokens :: ', accessToken, refreshToken); + try { + if (accessToken && refreshToken) { + // 토큰이 null이나 undefined가 아니면 저장 + await AsyncStorage.setItem(NT_ACCESS_TOKEN, accessToken); + await AsyncStorage.setItem(NT_REFRESH_TOKEN, refreshToken); + navigationRef.current.navigate('MainScreen'); + } else { + // 토큰이 null이나 undefined이면 제거 + await AsyncStorage.removeItem(NT_ACCESS_TOKEN); + await AsyncStorage.removeItem(NT_REFRESH_TOKEN); + } + } catch (error) { + console.error('토큰 저장 실패:', error); + } +}; + +// 토큰 가져오기 +export const loadTokens = async () => { + try { + const accessToken = await AsyncStorage.getItem(NT_ACCESS_TOKEN); + const refreshToken = await AsyncStorage.getItem(NT_REFRESH_TOKEN); + + if (accessToken === null) { + console.log('AccessToken is null'); + } + + if (refreshToken === null) { + console.log('RefreshToken is null'); + } + + if (accessToken && refreshToken) { + console.log('Tokens loaded:', { accessToken, refreshToken }); + + return { accessToken, refreshToken }; + } else { + console.log('No tokens found'); + // 토큰이 없으니까 여기서 로그인화면으로 보내자 + return null; + } + } catch (error) { + console.error('Failed to load tokens:', error); + return null; + } +}; + +// // 토큰 설정 및 이동 +export const setToken = async (tokenData: Object) => { + console.error('setToken -> tokenData', tokenData); + + if (!tokenData) { + console.error('Invalid tokens provided'); + return; + } + + const { access_token, refresh_token } = tokenData; + console.log('이거', access_token, refresh_token); + + try { + if (access_token && refresh_token) { + // 토큰이 null이나 undefined가 아니면 저장 + await AsyncStorage.setItem(NT_ACCESS_TOKEN, access_token); + await AsyncStorage.setItem(NT_REFRESH_TOKEN, refresh_token); + navigationRef.current.navigate('MainScreen'); + } else { + // 토큰이 null이나 undefined이면 제거 + // await AsyncStorage.removeItem(NT_ACCESS_TOKEN); + // await AsyncStorage.removeItem(NT_REFRESH_TOKEN); + } + } catch (error) { + console.error('토큰 저장 실패:', error); + } + // navigationRef.current.navigate('MainScreen'); +}; diff --git a/app/hooks/user/index.tsx b/app/hooks/user/index.tsx index 569cd35..db51214 100644 --- a/app/hooks/user/index.tsx +++ b/app/hooks/user/index.tsx @@ -3,10 +3,15 @@ import { useQuery } from '@tanstack/react-query'; import { userKeys } from '../../key/user'; export function useUser() { + // const token = get(); + const userInfo = useQuery({ queryKey: userKeys.info(), - queryFn: getUserInfoAPI, + queryFn: () => getUserInfoAPI(), + enabled: false, }); + console.log('@@ userInfo', userInfo); + return userInfo; } diff --git a/app/observers/app-state.tsx b/app/observers/app-state.tsx index fee97c1..f7d412a 100644 --- a/app/observers/app-state.tsx +++ b/app/observers/app-state.tsx @@ -13,7 +13,7 @@ export const AppStateComponent = () => { const subscription = AppState.addEventListener( 'change', async (nextAppState) => { - // console.log('@ 다음 동작 nextAppState', nextAppState); + console.log('@ 다음 동작 nextAppState', nextAppState); handleAppState(nextAppState); }, ); @@ -32,7 +32,7 @@ export const AppStateComponent = () => { appState.current.match(/inactive|background/) && nextAppState === 'active' ) { - // console.log('@ 앱으로 다시 돌아오는 경우 foreground'); + console.log('@ 앱으로 다시 돌아오는 경우 foreground'); } // 앱 비활성화 @@ -40,7 +40,7 @@ export const AppStateComponent = () => { appState.current.match(/inactive|active/) && nextAppState === 'background' ) { - // console.log('@ App has come to the background!'); + console.log('@ App has come to the background!'); } appState.current = nextAppState; // 변경된 상태를 바꿔줌. setUserAppState(nextAppState); diff --git a/app/screens/home.tsx b/app/screens/home.tsx index 39a3fc8..c075a16 100644 --- a/app/screens/home.tsx +++ b/app/screens/home.tsx @@ -53,6 +53,7 @@ const HomeScreen = ({ route, navigation }: Props) => { const { mapRef, listSheetRef, ListPoints, selectMarker, selectedPin } = useMapControl(); const { keyword } = route.params; + const userInfo = useUser(); const userData = userInfo?.data as unknown as userInfoType | undefined; const [selectValue, setSelectValue] = useState({ @@ -60,11 +61,12 @@ const HomeScreen = ({ route, navigation }: Props) => { cd: 'recent', }); - useFocusEffect(() => { - console.log('@@@@@@@@@@@@@@리패치'); - refetch(); - }); + // useFocusEffect(() => { + // // console.log('@@@@@@@@@@@@@@리패치'); + // refetch(); + // }); + console.log('userData', userData); const renderItem = useCallback( (item: PurchaseItemType, index: number) => ( @@ -97,7 +99,7 @@ const HomeScreen = ({ route, navigation }: Props) => { > - {userData?.college.name || '학교 선택'} + {userData?.college?.name || '학교 선택'} @@ -182,7 +184,7 @@ const HomeScreen = ({ route, navigation }: Props) => { )) : tradeList - ? tradeList.map(renderItem) + ? tradeList?.map(renderItem) : PURCHASE_ITEM_LIST.map(renderItem)} diff --git a/app/screens/index.tsx b/app/screens/index.tsx index 1c6fc9f..d60bbac 100644 --- a/app/screens/index.tsx +++ b/app/screens/index.tsx @@ -10,111 +10,83 @@ import { import styled from '@emotion/native'; import { Font16W500, UnderLine14 } from 'components/common/text'; import { getHeightRatio } from 'assets/util/layout'; -import NaverLogin, { NaverLoginRequest } from '@react-native-seoul/naver-login'; -import { GoogleSignin } from '@react-native-google-signin/google-signin'; -import * as KakaoLogin from '@react-native-seoul/kakao-login'; -import auth from '@react-native-firebase/auth'; -import { postLogin } from 'api/login'; import { getStorage, setStorage } from 'assets/util/storage'; -import { TOKEN_STORAGE_KEY } from 'assets/util/constants'; -import { SocialLoginRoute } from 'types/common'; -import { useUser } from 'hooks/user'; -import { RootStackParamList } from './stack'; -import { NativeStackScreenProps } from '@react-navigation/native-stack'; - -const naverLoginKeys = { - consumerKey: 'vnH89uX9Nczv8vOeXfQw', // 이거 필요한건가? - consumerSecret: 'TtWl5HamP7', // 얘도 필요한건가? - appName: 'nThing', - serviceUrlScheme: 'naverlogin', // only for iOS -}; +import { NT_ACCESS_TOKEN, NT_REFRESH_TOKEN } from 'assets/util/constants'; -const googleWebClientId = - '141023294009-g5k49bh6cmk0re3c94mnu9esi4ep3gcc.apps.googleusercontent.com'; - -const getServiceToken = (social: SocialLoginRoute, idToken: string) => - postLogin(social, { - id_token: idToken, - }).then((res) => { - console.log('응답', res); - return res.access_token as string; - }); - -const naverLogin = async (props: NaverLoginRequest) => { - try { - const { successResponse } = await NaverLogin.login(props); - console.log('네이버 토큰', successResponse?.accessToken); - } catch (e) { - console.warn(e); - } -}; - -const kakaoLogin = async () => { - try { - const { accessToken } = await KakaoLogin.login(); - console.log('카카오 토큰', accessToken); - // const res = await KakaoLogin.getProfile(); - // console.log('카카오 정뵤', res); - const token = await getServiceToken('kakao', accessToken); - console.log('service token', token); - return token; - } catch (e) { - console.warn(e); - } -}; -const googleLogin = async () => { - GoogleSignin.configure({ - webClientId: googleWebClientId, - }); - try { - const { idToken } = await GoogleSignin.signIn(); - // console.log('구글 토큰', idToken); - const googleCredential = auth.GoogleAuthProvider.credential(idToken); - const firebaseToken = await auth() - .signInWithCredential(googleCredential) - .then(({ user }) => user.getIdToken()); - // console.log('새로 받은 토큰', firebaseToken); - const token = await getServiceToken('google', firebaseToken); - return token; - } catch (e) { - console.warn(e); - } -}; +import { RootStackParamList } from './stack'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { googleLogin, kakaoLogin, naverLogin } from '~/assets/util/login'; +import { getUserInfoAPI } from '~/api/user'; +import AlertPopup from 'react-native-global-components/components/AlertPopup/AlertPopup'; +import { loadTokens } from '~/hooks/login/login'; type Props = NativeStackScreenProps; const RootScreen = ({ navigation }: Props) => { - const [serviceToken, setSeviceToken] = useState(); // 우리 서버에서 로그인 되고 나면 저장하려고 했음 - const userInfo = useUser(); - - console.log('@@@ userInfo', userInfo.data); - const setToken = (token?: string) => { - if (!token) return; - setSeviceToken(token); - setStorage(TOKEN_STORAGE_KEY, token); - }; + const [isLoggedIn, setIsLoggedIn] = React.useState(false); + + // useEffect(() => { + // const autoLogin = async () => { + // try { + // const { accessToken } = await loadTokens(); + // console.log('!!token', accessToken); + + // const isValidToken = await getUserInfoAPI(accessToken); + // console.log('@@isValidToken', isValidToken); + + // if (isValidToken) { + // navigation.navigate('MainScreen'); + // } else { + // // Handle invalid token case, maybe navigate to a login screen + // } + // } catch (error) { + // AlertPopup({ + // message: '다시 로그인해주세요.', + // }); + // console.error('Error fetching data:', error); + // // Handle error, perhaps by showing an alert or navigating to an error screen + // } + // }; + // autoLogin(); + // }, []); useEffect(() => { - if (!serviceToken) { - getStorage(TOKEN_STORAGE_KEY).then((data) => { - if (!data) return; - setSeviceToken(data); - }); - return; - } - console.log('user', userInfo); - // 1. serviceToken으로 user 정보 불러옴 (react query) - // 2-1. 선택한 학교가 없으면 학교 선택 페이지로 이동 - // navigation.navigate('UniversityScreen'); - // 2-2. 학교가 있으면 (학교 정보와 함께?) 홈으로 이동 - navigation.navigate('MainScreen'); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [serviceToken]); - - DevSettings.addMenuItem('Go Search Page', () => { - navigation.navigate('ChattingScreen'); - }); + const checkToken = async () => { + console.group('토큰 불러오기 시작'); + + const { accessToken } = await loadTokens(); // loadTokens의 결과를 기다림 + console.log('accessToken', accessToken); + + if (accessToken) { + try { + const isValidUser = await getUserInfoAPI(); + console.log('isValidUser', isValidUser); + if (isValidUser) { + setIsLoggedIn(true); + navigation.navigate('MainScreen'); + } else { + console.log('Invalid token, redirecting to login'); + setIsLoggedIn(false); + navigation.navigate('RootScreen'); + } + } catch (error) { + console.error('Token validation failed:', error); + // await AsyncStorage.removeItem(NT_ACCESS_TOKEN); + // await AsyncStorage.removeItem(NT_REFRESH_TOKEN); + setIsLoggedIn(false); + } + } else { + console.log('No valid tokens found, redirecting to login'); + setIsLoggedIn(false); + } + + console.groupEnd(); + }; + + checkToken(); + }, []); return ( @@ -122,15 +94,14 @@ const RootScreen = ({ navigation }: Props) => { NTHING + sns로 간편 로그인 - naverLogin(naverLoginKeys)}> + naverLogin()}> - kakaoLogin().then((token) => setToken(token))} - > + kakaoLogin()}> { +// if (!serviceToken) { +// getStorage(TOKEN_STORAGE_KEY).then((data) => { +// if (!data) return; +// setSeviceToken(data); +// }); +// return; +// } +// console.log('user', userInfo); +// // 1. serviceToken으로 user 정보 불러옴 (react query) +// // 2-1. 선택한 학교가 없으면 학교 선택 페이지로 이동 +// // navigation.navigate('UniversityScreen'); +// // 2-2. 학교가 있으면 (학교 정보와 함께?) 홈으로 이동 +// navigation.navigate('MainScreen'); +// // eslint-disable-next-line react-hooks/exhaustive-deps +// }, [serviceToken]); diff --git a/app/state/user.ts b/app/state/user.ts index e6ac58d..b1b3f7a 100644 --- a/app/state/user.ts +++ b/app/state/user.ts @@ -1,6 +1,18 @@ import { atom } from 'recoil'; +interface Token { + access_token: string; + refresh_token: string; +} export const DecodeTokenState = atom({ key: 'userInfo/DecodeTokenState', default: !false, // 로그인 상황일때는 true , 아닐때는 false }); + +export const TokenState = atom({ + key: 'userInfo/TokenState', + default: { + access_token: '', + refresh_token: '', + }, // 로그인 상황일때는 true , 아닐때는 false +}); diff --git a/app/types/user.ts b/app/types/user.ts index fe2503a..cc86b9c 100644 --- a/app/types/user.ts +++ b/app/types/user.ts @@ -1,5 +1,13 @@ import { WithIdName } from 'types/common'; +type collegeType = { + address: string; + name: string; + id: number; + latitude: string; + longitude: string; +}; + export interface userInfoType { id: number; nickname: string; @@ -7,13 +15,18 @@ export interface userInfoType { profile_image: string; credit: number; subscription_date: string; - college: WithIdName; + college: collegeType; provider_id: string; provider: 'google' | 'kakao' | 'naver'; } -export const enum APP_STATE { - active = 'active', // 앱 안에서 사용중인 경우 - inactive = 'inactive', // [IOS] 앱 안에서 벗어난 경우 - background = 'background', // 앱 안에서 다른곳으로 벗어난 경우 -} +export const APP_STATE = { + active: 'active', // 앱 안에서 사용중인 경우 + inactive: 'inactive', // [IOS] 앱 안에서 벗어난 경우 + background: 'background', // 앱 안에서 다른곳으로 벗어난 경우 +} as const; + +export const TOKEN_ERROR_STATUS = { + ACCESS_TOKEN_EXPIRED: 'ACCESS_TOKEN_EXPIRED', + REFRESH_TOKEN_EXPIRED: 'REFRESH_TOKEN_EXPIRED', +} as const; diff --git a/axiosConfig.ts b/axiosConfig.ts index c41dd81..cc6d21b 100644 --- a/axiosConfig.ts +++ b/axiosConfig.ts @@ -1,28 +1,76 @@ import axios, { Axios, AxiosRequestConfig } from 'axios'; import { AxiosResponse, CustomResponse } from '~/types/modules'; import { getStorage } from '~/assets/util/storage'; -import { TOKEN_STORAGE_KEY } from '~/assets/util/constants'; +import { SERVER_URL } from '~/assets/util/constants'; +import { refreshTokenAPI } from '~/api/login'; +import { navigationRef } from './RootNavigation'; +// import { useLogin } from '~/hooks/login/login'; export const AxiosConfig = () => { - axios.defaults.baseURL = 'https://422c-121-130-216-253.ngrok-free.app'; + axios.defaults.baseURL = SERVER_URL; + // const { setToken, loadTokens, serviceToken } = useLogin(); - axios.interceptors.request.use( - async (config) => { - const token = await getStorage(TOKEN_STORAGE_KEY); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; + // 호출 + // axios.interceptors.request.use( + // async (config) => { + // const { accessToken } = await loadTokens(); + // console.log('@@@ interceptors', accessToken); + // if (accessToken) { + // config.headers.Authorization = `Bearer ${accessToken}`; + // } + // return config; + // }, + // (error) => { + // console.log('axios.interceptors.request', error); + // return Promise.reject(error); + // }, + // ); + + // 응답 + axios.interceptors.response.use( + (response: AxiosResponse>) => { + console.log('@@ AXIOS RESPONSE 123', response.data); + return response.data.data; }, - (error) => { + async (error) => { + console.error('@@ AXIOS RESPONSE ERROR', error); + + if (error.response) { + const serverResponse = error.response.data; + console.error('@@ AXIOS RESPONSE ERROR-serverResponse', serverResponse); + + // api가 /user인 상황에서 Bearer값에 undefined가 들어가면 500값을 뱉음 + // 이때는 일단 RootScreen으로 보내야할듯 + // if (serverResponse.status === 500 && serverResponse.path == '/user') { + // navigationRef.current.navigate('RootScreen'); + // } + // Token 관련 에러 + if (serverResponse.status === 401) { + // console.error( + // '@@ AXIOS RESPONSE ERROR: 인증 오류 (401)', + // serverResponse, + // ); + // 액세스 토큰 만료 시 + if (serverResponse.message === 'ACCESS_TOKEN_EXPIRED') { + console.error( + '@@ AXIOS RESPONSE ERROR: 인증 오류 (401)', + serverResponse, + ); + // const new_access_token = await refreshTokenAPI(); + // console.log('axiosConfig.ts => new_access_token', new_access_token); + } + } + + // 필요하다면 사용자에게 알림 또는 다른 액션 + // alert(serverResponse.message); // 예시: "ACCESS_TOKEN_EXPIRED" 메시지 표시 + } else { + console.error('@@ AXIOS RESPONSE ERROR', error); + } + + // 오류를 그대로 전달 return Promise.reject(error); }, ); - axios.interceptors.response.use((response: AxiosResponse) => { - console.log('@@ AXIOS RESPONSE 123', response); - return response.data.data; - }); - return null; }; diff --git a/metro.config.js b/metro.config.js index 7cb0cd5..78a711e 100644 --- a/metro.config.js +++ b/metro.config.js @@ -18,9 +18,14 @@ module.exports = (async () => { transform: { experimentalImportSupport: false, inlineRequires: true, + nonInlinedRequires: [ + '@react-native-async-storage/async-storage', + 'React', + 'react', + 'react-native', + ], // storybook changed log 웹에서만 가능.. // inlineRequires: false, - }, }), babelTransformerPath: require.resolve('react-native-svg-transformer'),