From 91e516dccda3ec3db7a7b83e2fe4fd2a85719cf9 Mon Sep 17 00:00:00 2001 From: Jiwon Chae <63784453+jiwon0226@users.noreply.github.com> Date: Fri, 9 May 2025 16:09:39 +0900 Subject: [PATCH 1/2] =?UTF-8?q?KW-313/feat:=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=A0=81=EC=9A=A9=20-=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8,=20=EC=9A=94=EC=B2=AD=20=EC=9D=B8=ED=84=B0=EC=85=89?= =?UTF-8?q?=ED=8A=B8=EC=99=80=20=EC=9D=91=EB=8B=B5=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=EC=85=89=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AppNavigator.js | 24 +++++++- apis/AxiosInstance.js | 104 +++++++++++++++++++++++++++++++++ app.config.js | 2 +- handler/logoutHandler.js | 11 ++++ modals/PasswordConfirmModal.js | 42 ++++++++----- 5 files changed, 165 insertions(+), 18 deletions(-) create mode 100644 handler/logoutHandler.js diff --git a/AppNavigator.js b/AppNavigator.js index f972c88..fc4dd6e 100644 --- a/AppNavigator.js +++ b/AppNavigator.js @@ -5,8 +5,10 @@ import Ionicons from '@expo/vector-icons/Ionicons'; import { StatusBar, View } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import { setLogoutCallback } from './handler/logoutHandler'; import HomeButtonController from './components/buttons/HomeButtonController'; import LoadingOverlay from './components/loadings/LoadingOverlay'; +import { getMyInfo } from './apis/MyPageApi'; // 로그인 전 페이지 import WelcomePage from './pages/WelcomePage'; @@ -32,15 +34,26 @@ export default function AppNavigator() { const [navState, setNavState] = useState(null); const [loading, setLoading] = useState(false); // 토큰 확인 중 상태 - // 앱 시작 시 토큰 확인 + // 앱 시작 시 토큰 유효성 확인 useEffect(() => { const checkToken = async () => { setLoading(true); try { - // 로딩 돌아가는거 강제로 1초 보기 await new Promise((resolve) => setTimeout(resolve, 1000)); const token = await AsyncStorage.getItem('accessToken'); - setIsLoggedIn(!!token); // 토큰 있으면 true + if (token) { + // 회원 정보 조회로 토큰 유효성 검증 + try { + await getMyInfo(token); + setIsLoggedIn(true); // 토큰 유효 + } catch (err) { + //에러 발생 시 + setIsLoggedIn(false); + await AsyncStorage.removeItem('accessToken'); + } + } else { + setIsLoggedIn(false); + } } catch (e) { setIsLoggedIn(false); } finally { @@ -50,6 +63,11 @@ export default function AppNavigator() { checkToken(); }, []); + // 앱 시작시 logoutHandler.js에 콜백 함수 등록 + useEffect(() => { + setLogoutCallback(() => setIsLoggedIn(false)); + }, []); + const navTheme = { ...DefaultTheme, colors: { diff --git a/apis/AxiosInstance.js b/apis/AxiosInstance.js index 1d01aa6..8bb45eb 100644 --- a/apis/AxiosInstance.js +++ b/apis/AxiosInstance.js @@ -1,5 +1,7 @@ import axios from 'axios'; import Constants from 'expo-constants'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { runLogoutCallback } from '../handler/logoutHandler'; const BASE_URL = Constants.expoConfig.extra.BASE_URL; @@ -8,4 +10,106 @@ const instance = axios.create({ withCredentials: true, }); +// accessToken 가져오는 함수 +const getAccessToken = async () => { + return await AsyncStorage.getItem('accessToken'); +}; + +// 요청 인터셉터: 모든 요청에 accessToken 자동 첨부 +instance.interceptors.request.use( + async (config) => { + const token = await getAccessToken(); + if (token) { + config.headers = config.headers || {}; // headers가 없으면 빈 객체로 초기화 + config.headers.Authorization = `Bearer ${token}`; // Authorization 헤더에 Bearer 토큰 추가 + } + // 요청 정보 로그 + // console.log('[axios request] url:', config.url); + // console.log('[axios request] headers:', config.headers); + return config; + }, + (error) => Promise.reject(error), +); + +// 응답 인터셉터: 401(accessToken 만료) → 토큰 재발급 후 재요청 +instance.interceptors.response.use( + (response) => { + // 응답 로그 + // console.log('[axios response] url:', response.config.url); + // console.log('[axios response] status:', response.status); + return response; + }, + async (error) => { + const originalRequest = error.config; + // 401 에러이면서 아직 재시도 하지 않은 요청만 처리 + if (error.response && error.response.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + try { + // 토큰 재발급 요청 + const refreshResponse = await axios.post( + '/auth/reissue', + {}, + { + baseURL: BASE_URL, + withCredentials: true, + headers: { + Authorization: `Bearer ${await getAccessToken()}`, + }, + }, + ); + //재발급 응답 헤더 로그 + //console.log('refreshResponse.headers:', refreshResponse.headers); + + //authorization, Authorization 대소문자 상관없시 추출하도록함 + //새 accessToken을 헤더에서 추출 + let newAccessToken = refreshResponse.headers['authorization']; + //새 accessTokem 로그 + //console.log('newAccessToken:', newAccessToken); + if (!newAccessToken) { + newAccessToken = refreshResponse.headers['Authorization']; + } + // newAccessToken이 있으면, 'Bearer ' 접두사 제거 후 공백 제거 후 AsyncStorage에 저장 + if (newAccessToken) { + const tokenValue = newAccessToken.replace('Bearer ', '').trim(); + await AsyncStorage.setItem('accessToken', tokenValue); + + // 기존 헤더는 spread로 보존, Authorization만 교체 + originalRequest.headers = { + ...originalRequest.headers, + Authorization: `Bearer ${tokenValue}`, + }; + + // POST/PUT 등일 때 data 유실 방지 + if ( + ['post', 'put', 'patch'].includes(originalRequest.method) && + !originalRequest.data && // originalRequest에 data가 없고 + error.config.data // 에러 config에 data가 있으면 + ) { + originalRequest.data = error.config.data; // data를 복구 + } + + // 서버가 토큰 바로 반영 안 할 때를 대비해 약간 딜레이 + await new Promise((resolve) => setTimeout(resolve, 200)); + + // 재요청 전 로그 + //console.log('재요청 config:', originalRequest); + + //새 accessToken으로 원래 요청을 재시도 + return instance(originalRequest); + } + //새 accessToken을 받지 못한 경우 에러 반환 + return Promise.reject(new Error('새로운 accessToken을 받지 못했습니다.')); + } catch (refreshError) { + //토큰 재발급 실패 시 + await AsyncStorage.removeItem('accessToken'); + // 로그아웃 콜백 실행 + runLogoutCallback(); + return Promise.reject(refreshError); //에러 반환 + } + } + //그 외의 에러 그대로 반환 + return Promise.reject(error); + }, +); + export default instance; diff --git a/app.config.js b/app.config.js index 37b8242..64e96b5 100644 --- a/app.config.js +++ b/app.config.js @@ -25,7 +25,7 @@ export default { favicon: './assets/images/logoIcon.png', }, extra: { - BASE_URL: 'http://192.168.0.115:8081', // 본인 pc IPv4 주소로 수정하세용 + BASE_URL: 'http://192.168.0.111:8081', // 본인 pc IPv4 주소로 수정하세용 }, }, }; diff --git a/handler/logoutHandler.js b/handler/logoutHandler.js new file mode 100644 index 0000000..471cd66 --- /dev/null +++ b/handler/logoutHandler.js @@ -0,0 +1,11 @@ +// 콜백 함수 저장하는 변수 +let logoutCallback = null; + +export function setLogoutCallback(cb) { + logoutCallback = cb; +} + +// 설정된 콜백 함수를 실행하는 함수 +export function runLogoutCallback() { + if (logoutCallback) logoutCallback(); +} diff --git a/modals/PasswordConfirmModal.js b/modals/PasswordConfirmModal.js index f381379..3a574a2 100644 --- a/modals/PasswordConfirmModal.js +++ b/modals/PasswordConfirmModal.js @@ -21,30 +21,44 @@ const PasswordConfirmModal = ({ visible = true, onCloseHandler }) => { setPassword(text); if (!isValidPassword(text)) { - setErrorText('비밀번호는 8자 이상, 영문/숫자/특수문자를 포함해야 합니다.'); + setErrorText('8자 이상, 영문/숫자/특수문자를 포함해야 합니다.'); } else { setErrorText(''); } }; + // const handleConfirm = async () => { + // if (!isValidPassword(password)) { + // setErrorText('8자 이상, 영문/숫자/특수문자를 포함해야 합니다.'); + // return; + // } + + // try { + // // 토큰 불러오기 + // const token = await AsyncStorage.getItem('accessToken'); + // if (!token) { + // throw new Error('토큰이 존재하지 않습니다.'); + // } + + // await verifyPassword(token, password); + + // // 모달창 닫기 + // onCloseHandler(); + // } catch (error) { + // console.log(error); + // setErrorText('비밀번호가 일치하지 않습니다. 다시 입력해주세요.'); + // } + // }; const handleConfirm = async () => { if (!isValidPassword(password)) { - setErrorText('비밀번호는 8자 이상, 영문/숫자/특수문자를 포함해야 합니다.'); + setErrorText('8자 이상, 영문/숫자/특수문자를 포함해야 합니다.'); return; } - try { - // 토큰 불러오기 - const token = await AsyncStorage.getItem('accessToken'); - if (!token) { - throw new Error('토큰이 존재하지 않습니다.'); - } - - await verifyPassword(token, password); - - // 모달창 닫기 - onCloseHandler(); - } catch (error) { + // 임시: 비밀번호가 'Lgcns01!'이면 성공, 아니면 실패 + if (password === 'Lgcns01!') { + onCloseHandler(); // 성공 시 모달 닫기 + } else { setErrorText('비밀번호가 일치하지 않습니다. 다시 입력해주세요.'); } }; From 79131535be4c16d14ab8362c116471b3c1e46c31 Mon Sep 17 00:00:00 2001 From: Jiwon Chae <63784453+jiwon0226@users.noreply.github.com> Date: Fri, 9 May 2025 16:37:08 +0900 Subject: [PATCH 2/2] =?UTF-8?q?KW-313/api=EC=97=90=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=20=EC=88=98=EC=A0=95(=EC=9D=B8=EC=8A=A4?= =?UTF-8?q?=ED=84=B4=EC=8A=A4=EC=97=90=EC=84=9C=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AppNavigator.js | 2 +- apis/MyPageApi.js | 26 ++++-------------- apis/PasswordApi.js | 32 +++++------------------ modals/PasswordConfirmModal.js | 48 ++++++++++++++++------------------ pages/ChangePasswordPage.js | 6 +---- pages/MyPage.js | 20 +++----------- 6 files changed, 40 insertions(+), 94 deletions(-) diff --git a/AppNavigator.js b/AppNavigator.js index fc4dd6e..f67bdcc 100644 --- a/AppNavigator.js +++ b/AppNavigator.js @@ -44,7 +44,7 @@ export default function AppNavigator() { if (token) { // 회원 정보 조회로 토큰 유효성 검증 try { - await getMyInfo(token); + await getMyInfo(); setIsLoggedIn(true); // 토큰 유효 } catch (err) { //에러 발생 시 diff --git a/apis/MyPageApi.js b/apis/MyPageApi.js index d6cc42b..d342835 100644 --- a/apis/MyPageApi.js +++ b/apis/MyPageApi.js @@ -1,38 +1,22 @@ import axios from './AxiosInstance'; // 회원 정보 조회 함수 -export const getMyInfo = async (token) => { - const response = await axios.get('/members/me', { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - +export const getMyInfo = async () => { + const response = await axios.get('/members/me'); return response.data.data; }; // 로그아웃 함수 -export const logoutUser = async (token) => { +export const logoutUser = async () => { const response = await axios.post( '/auth/logout', {}, // body 부분에 빈 객체 명시 - { - headers: { - Authorization: `Bearer ${token}`, - }, - }, ); - return response.status; }; // 회원 탈퇴 함수 -export const deleteUser = async (token) => { - const response = await axios.delete('/members', { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - +export const deleteUser = async () => { + const response = await axios.delete('/members'); return response.status; }; diff --git a/apis/PasswordApi.js b/apis/PasswordApi.js index f2e857e..9c30b44 100644 --- a/apis/PasswordApi.js +++ b/apis/PasswordApi.js @@ -1,34 +1,16 @@ import axios from './AxiosInstance'; // 마이 페이지 진입 시, 비밀번호 검증 함수 -export const verifyPassword = async (token, password) => { - const response = await axios.post( - '/members/me/password', - { password }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - }, - ); - +export const verifyPassword = async (password) => { + const response = await axios.post('/members/me/password', { password }); return response.data; }; // 비밀번호 변경 함수 -export const updatePassword = async (token, data) => { - const response = await axios.patch( - '/members/me/password', - { - passwordOriginal: data.originalPassword, - passwordNew: data.newPassword, - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - }, - ); - +export const updatePassword = async (data) => { + const response = await axios.patch('/members/me/password', { + passwordOriginal: data.originalPassword, + passwordNew: data.newPassword, + }); return response.data; }; diff --git a/modals/PasswordConfirmModal.js b/modals/PasswordConfirmModal.js index 4fa78dd..d25784c 100644 --- a/modals/PasswordConfirmModal.js +++ b/modals/PasswordConfirmModal.js @@ -27,41 +27,37 @@ const PasswordConfirmModal = ({ visible = true, onCloseHandler }) => { } }; - // const handleConfirm = async () => { - // if (!isValidPassword(password)) { - // setErrorText('8자 이상, 영문/숫자/특수문자를 포함해야 합니다.'); - // return; - // } - - // try { - // // 토큰 불러오기 - // const token = await AsyncStorage.getItem('accessToken'); - // if (!token) { - // throw new Error('토큰이 존재하지 않습니다.'); - // } - - // await verifyPassword(token, password); - - // // 모달창 닫기 - // onCloseHandler(); - // } catch (error) { - // console.log(error); - // setErrorText('비밀번호가 일치하지 않습니다. 다시 입력해주세요.'); - // } - // }; const handleConfirm = async () => { if (!isValidPassword(password)) { setErrorText('8자 이상, 영문/숫자/특수문자를 포함해야 합니다.'); return; } - // 임시: 비밀번호가 'Lgcns01!'이면 성공, 아니면 실패 - if (password === 'Lgcns01!') { - onCloseHandler(); // 성공 시 모달 닫기 - } else { + try { + // 토큰 불러오기 + const token = await AsyncStorage.getItem('accessToken'); + await verifyPassword(password); + + // 모달창 닫기 + onCloseHandler(); + } catch (error) { + console.log(error); setErrorText('비밀번호가 일치하지 않습니다. 다시 입력해주세요.'); } }; + // const handleConfirm = async () => { + // if (!isValidPassword(password)) { + // setErrorText('8자 이상, 영문/숫자/특수문자를 포함해야 합니다.'); + // return; + // } + + // // 임시: 비밀번호가 'Lgcns01!'이면 성공, 아니면 실패 + // if (password === 'Lgcns01!') { + // onCloseHandler(); // 성공 시 모달 닫기 + // } else { + // setErrorText('비밀번호가 일치하지 않습니다. 다시 입력해주세요.'); + // } + // }; return ( diff --git a/pages/ChangePasswordPage.js b/pages/ChangePasswordPage.js index 64c5973..f87b793 100644 --- a/pages/ChangePasswordPage.js +++ b/pages/ChangePasswordPage.js @@ -51,11 +51,7 @@ const ChangePasswordPage = () => { try { // 토큰 불러오기 const token = await AsyncStorage.getItem('accessToken'); - if (!token) { - throw new Error('토큰이 존재하지 않습니다.'); - } - - await updatePassword(token, { originalPassword, newPassword }); + await updatePassword({ originalPassword, newPassword }); setTimeout(() => { setShowSuccessAlert(true); diff --git a/pages/MyPage.js b/pages/MyPage.js index da694cd..9c94625 100644 --- a/pages/MyPage.js +++ b/pages/MyPage.js @@ -47,7 +47,7 @@ export default function MyPage({ setIsLoggedIn }) { setAccessToken(token); - const data = await getMyInfo(token); + const data = await getMyInfo(); setUserInfo({ name: data.name, birth: data.birthDate, @@ -78,15 +78,9 @@ export default function MyPage({ setIsLoggedIn }) { // 로그아웃 처리 try { - if (!accessToken) { - throw new Error('토큰이 존재하지 않습니다.'); - } - - await logoutUser(accessToken); - + await logoutUser(); // 토큰 삭제 await AsyncStorage.removeItem('accessToken'); - // 시작 페이지로 이동되도록 로그인 상태 설정 setIsLoggedIn(false); } catch (error) { @@ -113,15 +107,9 @@ export default function MyPage({ setIsLoggedIn }) { // 회원 탈퇴 처리 try { - if (!accessToken) { - throw new Error('토큰이 존재하지 않습니다.'); - } - - await deleteUser(accessToken); - + await deleteUser(); // 토큰 삭제 await AsyncStorage.removeItem('accessToken'); - // 시작 페이지로 이동되도록 로그인 상태 설정 setIsLoggedIn(false); } catch (error) { @@ -186,7 +174,7 @@ export default function MyPage({ setIsLoggedIn }) { />