diff --git a/src/apis/auth.ts b/src/apis/auth.ts index dce8e3fe..80671de4 100644 --- a/src/apis/auth.ts +++ b/src/apis/auth.ts @@ -16,7 +16,7 @@ export const postReIssueAccessToken = async (): Promise => { }; /** - * Google OAuth2 로그인 URL로 리다이렉트 + * Google OAuth2 로그인 요청 함수 * - components/Onboarding/SocialLoginButton.tsx */ export const redirectToGoogleLogin = () => { @@ -36,7 +36,7 @@ export const redirectToGoogleLogin = () => { }; /** - * Kakao OAuth2 로그인 URL로 리다이렉트 + * Kakao OAuth2 로그인 요청 함수 * - components/Onboarding/SocialLoginButton.tsx */ export const redirectToKakaoLogin = () => { diff --git a/src/apis/axios.ts b/src/apis/axios.ts index 59f10221..071bbd8e 100644 --- a/src/apis/axios.ts +++ b/src/apis/axios.ts @@ -2,15 +2,12 @@ import axios, { type InternalAxiosRequestConfig } from 'axios'; import { useLocalStorage } from '../hooks/useLocalStorage'; import { LOCAL_STORAGE_KEY } from '../constants/key'; -// 커스텀 인터페이스: 재시도 여부를 위한 플래그 추가 interface CustomInternalAxiosRequestConfig extends InternalAxiosRequestConfig { _retry?: boolean; } -// accessToken 재발급 요청 중복 방지를 위한 전역 변수 let tokenReissuePromise: Promise | null = null; -// 기본 axios 인스턴스 export const axiosInstance = axios.create({ baseURL: import.meta.env.VITE_SERVER_API_URL, withCredentials: true, @@ -45,46 +42,40 @@ axiosInstance.interceptors.response.use( async (error) => { const originalRequest: CustomInternalAxiosRequestConfig = error.config; - // accessToken이 만료된 경우 && 아직 재시도한 적 없는 경우 if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; - // 재발급 요청 자체가 실패한 경우 → 온보딩 처음 페이지로 이동 if (originalRequest.url?.includes('/api/token/reissue')) { const { removeItem } = useLocalStorage(LOCAL_STORAGE_KEY.accessToken); - removeItem(); // accessToken 삭제 + removeItem(); window.location.href = '/onboarding'; return Promise.reject(error); } - // 이미 진행 중인 refresh 요청이 없으면 실행 if (!tokenReissuePromise) { tokenReissuePromise = axiosInstance .post('/api/token/reissue', null, { - withCredentials: true, // 쿠키 포함 (refreshToken) + withCredentials: true, }) .then((res) => { const newAccessToken = res.data.result?.accessToken; if (!newAccessToken) throw new Error('accessToken 발급 실패'); const { setItem } = useLocalStorage(LOCAL_STORAGE_KEY.accessToken); - setItem(newAccessToken); // 새로운 accessToken 저장 + setItem(newAccessToken); return newAccessToken; }) .catch((err) => { const { removeItem } = useLocalStorage(LOCAL_STORAGE_KEY.accessToken); - removeItem(); // 실패 시 accessToken 삭제 + removeItem(); window.location.href = '/onboarding'; return Promise.reject(err); }) .finally(() => { - // 다음 요청에서 재시도 가능하도록 초기화 tokenReissuePromise = null; }); } - - // 재발급 성공 시 -> 기존 요청에 새 accessToken 붙여서 재전송 return tokenReissuePromise.then((newAccessToken) => { originalRequest.headers = originalRequest.headers || {}; originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; diff --git a/src/components/Onboarding/CopyToClipboard.tsx b/src/components/Onboarding/CopyToClipboard.tsx index c5e856ad..4cb518b1 100644 --- a/src/components/Onboarding/CopyToClipboard.tsx +++ b/src/components/Onboarding/CopyToClipboard.tsx @@ -3,29 +3,26 @@ import copy from '../../assets/icons/copy.svg'; import speechbubblebody from '../../assets/icons/speechbubblebody.svg'; import speechbubbletail from '../../assets/icons/speechbubbletail.svg'; -// props로 전달받은 inputRef를 통해 복사 대상 엘리먼트에 접근 interface CopyToClipboardProps { inputRef: React.RefObject; } const CopyToClipboard = ({ inputRef }: CopyToClipboardProps) => { - const [copied, setCopied] = useState(false); // 복사 완료 여부 상태 + const [copied, setCopied] = useState(false); const handleCopy = () => { try { if (inputRef.current) { const textarea = inputRef.current; - - // 일부 iOS 브라우저에서 select() 동작이 안 되는 이슈를 피하기 위해 readOnly로 설정 textarea.readOnly = true; - textarea.select(); // 전체 텍스트 선택 + textarea.select(); - const success = document.execCommand('copy'); // 복사 시도 - textarea.readOnly = false; // 다시 원래대로 복원 + const success = document.execCommand('copy'); + textarea.readOnly = false; if (success) { - setCopied(true); // 복사 성공 시 상태를 true로 변경 (말풍선 띄우기 위함) - setTimeout(() => setCopied(false), 2000); // 2초 후 다시 false로 (말풍선 사라짐) + setCopied(true); + setTimeout(() => setCopied(false), 2000); } else { alert('복사에 실패했습니다. 직접 복사해주세요.'); } @@ -39,19 +36,16 @@ const CopyToClipboard = ({ inputRef }: CopyToClipboardProps) => { return ( ); }; diff --git a/src/components/Onboarding/SocialLoginButton.tsx b/src/components/Onboarding/SocialLoginButton.tsx index 682c94cc..69865e15 100644 --- a/src/components/Onboarding/SocialLoginButton.tsx +++ b/src/components/Onboarding/SocialLoginButton.tsx @@ -2,14 +2,11 @@ import googlelogo from '../../assets/logos/googlelogo.png'; import kakaologo from '../../assets/logos/kakaologo.svg'; import { redirectToGoogleLogin, redirectToKakaoLogin } from '../../apis/auth'; -// 컴포넌트에 넘길 props 정의 interface SocialLoginButtonProps { - provider: 'google' | 'kakao'; // 로그인 버튼 종류 (google 또는 kakao) + provider: 'google' | 'kakao'; } -// SocialLoginButton 컴포넌트 정의 const SocialLoginButton = ({ provider }: SocialLoginButtonProps) => { - // provider에 따라 리다이렉트 const handleClick = async () => { try { if (provider === 'google') { @@ -22,14 +19,12 @@ const SocialLoginButton = ({ provider }: SocialLoginButtonProps) => { } }; - // 구글 로그인 버튼 렌더링 if (provider === 'google') { return (