From 6b1cfed497124895285c5e939cac02b4984e2b47 Mon Sep 17 00:00:00 2001 From: heeyongKim <166043860+heeeeyong@users.noreply.github.com> Date: Sun, 17 Aug 2025 16:00:37 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EC=86=8C=EC=85=9C=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20API=20=ED=98=B8=EC=B6=9C=20=EB=B9=84=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useSocialLoginToken.ts | 19 ++++++++++++++++--- src/pages/feed/Feed.tsx | 21 ++++++++++++++------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/hooks/useSocialLoginToken.ts b/src/hooks/useSocialLoginToken.ts index e9818796..724f3453 100644 --- a/src/hooks/useSocialLoginToken.ts +++ b/src/hooks/useSocialLoginToken.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { apiClient } from '@/api/index'; @@ -6,6 +6,9 @@ export const useSocialLoginToken = () => { const navigate = useNavigate(); const location = useLocation(); + // 토큰 발급 완료를 기다리는 Promise + const tokenPromise = useRef | null>(null); + useEffect(() => { const handleSocialLoginToken = async () => { // URL에서 loginTokenKey 가져오기 @@ -72,7 +75,17 @@ export const useSocialLoginToken = () => { const isSocialLoginComplete = urlParams.get('loginTokenKey'); if (isSocialLoginComplete) { - handleSocialLoginToken(); + // 토큰 발급 Promise를 저장 + tokenPromise.current = handleSocialLoginToken(); + } + }, [location.pathname, navigate]); + + // 토큰 발급 완료를 기다리는 함수 반환 + const waitForToken = async (): Promise => { + if (tokenPromise.current) { + await tokenPromise.current; } - }, [location.pathname, navigate]); // location.pathname과 navigate만 의존성으로 설정 + }; + + return { waitForToken }; }; diff --git a/src/pages/feed/Feed.tsx b/src/pages/feed/Feed.tsx index e3febc52..be82bf93 100644 --- a/src/pages/feed/Feed.tsx +++ b/src/pages/feed/Feed.tsx @@ -21,7 +21,7 @@ const Feed = () => { const [activeTab, setActiveTab] = useState(initialTabFromState ?? tabs[0]); // 소셜 로그인 토큰 발급 처리 - useSocialLoginToken(); + const { waitForToken } = useSocialLoginToken(); // 최초 마운트 시에만 history state 제거하여 이후 재방문 시 영향 없도록 처리 useEffect(() => { @@ -146,12 +146,19 @@ const Feed = () => { // 탭별로 API 호출 useEffect(() => { - if (activeTab === '피드') { - loadTotalFeeds(); - } else if (activeTab === '내 피드') { - loadMyFeeds(); - } - }, [activeTab]); + const loadFeedsWithToken = async () => { + // 토큰 발급 완료 대기 + await waitForToken(); + + if (activeTab === '피드') { + loadTotalFeeds(); + } else if (activeTab === '내 피드') { + loadMyFeeds(); + } + }; + + loadFeedsWithToken(); + }, [activeTab, waitForToken]); return ( From a643c5be4c5d18715e97df9978d335b5ef98b46d Mon Sep 17 00:00:00 2001 From: heeyongKim <166043860+heeeeyong@users.noreply.github.com> Date: Sun, 17 Aug 2025 17:50:37 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EC=88=98=EC=A0=95=20(accessToken=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth/exchangeTempToken.ts | 23 ---------- src/api/auth/getToken.ts | 19 ++++++++ src/api/auth/index.ts | 3 +- src/api/auth/setCookie.ts | 15 ------- src/api/index.ts | 26 ++++------- src/api/users/postSignup.ts | 3 +- src/hooks/useSocialLoginToken.ts | 69 +++++++++-------------------- src/pages/feed/Feed.tsx | 17 ++++--- src/pages/login/Login.tsx | 13 ++++++ src/pages/signup/SignupGenre.tsx | 17 +++---- src/pages/signup/SignupNickname.tsx | 24 +++++----- 11 files changed, 101 insertions(+), 128 deletions(-) delete mode 100644 src/api/auth/exchangeTempToken.ts create mode 100644 src/api/auth/getToken.ts delete mode 100644 src/api/auth/setCookie.ts diff --git a/src/api/auth/exchangeTempToken.ts b/src/api/auth/exchangeTempToken.ts deleted file mode 100644 index 49b33a7c..00000000 --- a/src/api/auth/exchangeTempToken.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { apiClient } from '../index'; - -export type ExchangeTempTokenRequest = Record; - -export interface ExchangeTempTokenResponse { - isSuccess: boolean; - code: number; - message: string; - data: { - accessToken: string; - // 기타 필요한 응답 데이터 - }; -} - -export const exchangeTempToken = async ( - data?: ExchangeTempTokenRequest, -): Promise => { - const response = await apiClient.post( - '/auth/exchange-temp-token', - data, - ); - return response.data; -}; diff --git a/src/api/auth/getToken.ts b/src/api/auth/getToken.ts new file mode 100644 index 00000000..8f9fa37d --- /dev/null +++ b/src/api/auth/getToken.ts @@ -0,0 +1,19 @@ +import { apiClient } from '../index'; + +export interface GetTokenRequest { + loginTokenKey: string; // 인가코드 +} + +export interface GetTokenResponse { + isSuccess: boolean; + code: number; + message: string; + data: { + token: string; // 토큰 + }; +} + +export const getToken = async (data: GetTokenRequest): Promise => { + const response = await apiClient.post('/auth/token', data); + return response.data; +}; diff --git a/src/api/auth/index.ts b/src/api/auth/index.ts index 98c28316..b5dac0eb 100644 --- a/src/api/auth/index.ts +++ b/src/api/auth/index.ts @@ -1,2 +1 @@ -export { setCookie } from './setCookie'; -export { exchangeTempToken } from './exchangeTempToken'; +export { getToken } from './getToken'; diff --git a/src/api/auth/setCookie.ts b/src/api/auth/setCookie.ts deleted file mode 100644 index 5d1a2702..00000000 --- a/src/api/auth/setCookie.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { apiClient } from '../index'; - -export type SetCookieRequest = Record; - -export interface SetCookieResponse { - isSuccess: boolean; - code: number; - message: string; - data: Record; -} - -export const setCookie = async (data?: SetCookieRequest): Promise => { - const response = await apiClient.post('/auth/set-cookie', data); - return response.data; -}; diff --git a/src/api/index.ts b/src/api/index.ts index 043a2e00..79258320 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -17,25 +17,18 @@ export const apiClient = axios.create({ // const TEMP_ACCESS_TOKEN = // 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDM4MjY1MiwiZXhwIjoxNzU2OTc0NjUyfQ.BSGuoMWlrzc0oKgSJXHEycxdzzY9-e7gD4xh-wSDemc'; -// Request 인터셉터: temp_token과 access_token 쿠키 처리 +// Request 인터셉터: localStorage의 토큰을 헤더에 자동 추가 apiClient.interceptors.request.use( config => { - // 쿠키에서 temp_token과 access_token 확인 - const cookies = document.cookie.split(';'); - const hasTempToken = cookies.some(cookie => cookie.trim().startsWith('temp_token=')); - const hasAccessToken = cookies.some(cookie => cookie.trim().startsWith('access_token=')); + // localStorage에서 토큰 확인 + const authToken = localStorage.getItem('authToken'); - if (hasAccessToken) { - // access_token이 있으면 정상 토큰 사용 - console.log('✅ access_token 쿠키가 있어서 정상 토큰을 사용합니다.'); - // access_token은 withCredentials: true로 자동 전송되므로 별도 헤더 설정 불필요 - } else if (hasTempToken) { - // temp_token이 있으면 임시 토큰 사용 - console.log('🔑 temp_token 쿠키가 있어서 임시 토큰을 사용합니다.'); - // temp_token도 withCredentials: true로 자동 전송되므로 별도 헤더 설정 불필요 + if (authToken) { + // 토큰이 있으면 Authorization 헤더에 추가 + console.log('🔑 Authorization 헤더에 토큰 추가'); + config.headers.Authorization = `Bearer ${authToken}`; } else { - // 둘 다 없으면 인증 토큰 없음 - console.log('❌ temp_token과 access_token 쿠키가 모두 없습니다.'); + console.log('❌ localStorage에 토큰이 없습니다.'); } return config; @@ -50,8 +43,7 @@ apiClient.interceptors.response.use( (response: AxiosResponse) => response, (error: AxiosError) => { if (error.response?.status === 401) { - // 인증 실패 시 로그인 페이지로 리다이렉트 - // window.location.href = '/'; + window.location.href = '/'; } return Promise.reject(error); }, diff --git a/src/api/users/postSignup.ts b/src/api/users/postSignup.ts index dfc001d5..95ed94e0 100644 --- a/src/api/users/postSignup.ts +++ b/src/api/users/postSignup.ts @@ -7,11 +7,12 @@ export interface PostSignupRequest { } export interface PostSignupResponse { - success: boolean; + isSuccess: boolean; code: number; message: string; data: { userId: number; + accessToken: string; // 회원가입 완료 후 받는 access 토큰 }; } diff --git a/src/hooks/useSocialLoginToken.ts b/src/hooks/useSocialLoginToken.ts index 724f3453..36e8924c 100644 --- a/src/hooks/useSocialLoginToken.ts +++ b/src/hooks/useSocialLoginToken.ts @@ -1,16 +1,15 @@ -import { useEffect, useRef } from 'react'; -import { useNavigate, useLocation } from 'react-router-dom'; -import { apiClient } from '@/api/index'; +import { useEffect, useRef, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import { getToken } from '@/api/auth'; export const useSocialLoginToken = () => { - const navigate = useNavigate(); const location = useLocation(); // 토큰 발급 완료를 기다리는 Promise const tokenPromise = useRef | null>(null); useEffect(() => { - const handleSocialLoginToken = async () => { + const handleSocialLoginToken = async (): Promise => { // URL에서 loginTokenKey 가져오기 const params = new URLSearchParams(window.location.search); const loginTokenKey = params.get('loginTokenKey'); @@ -20,53 +19,29 @@ export const useSocialLoginToken = () => { return; } - // 현재 경로가 /signup인지 확인 - const isSignupPage = location.pathname === '/signup'; - try { - if (isSignupPage) { - // 회원가입 페이지인 경우: 임시토큰 발급 요청 - console.log('🔑 회원가입 페이지: 임시토큰 발급 요청'); - console.log('📋 loginTokenKey:', loginTokenKey); + console.log('🔑 소셜 로그인 토큰 발급 요청'); + console.log('📋 loginTokenKey:', loginTokenKey); - const response = await apiClient.post( - '/auth/set-cookie', - { loginTokenKey }, - { withCredentials: true }, - ); + // /auth/token API 호출하여 토큰 발급 (임시 토큰 또는 access 토큰) + const response = await getToken({ loginTokenKey }); - if (response.data.isSuccess) { - console.log('✅ 임시토큰 발급 성공'); - // URL에서 loginTokenKey 파라미터 제거 - const newUrl = window.location.pathname; - window.history.replaceState({}, document.title, newUrl); - } else { - console.error('❌ 임시토큰 발급 실패:', response.data.message); - } - } else { - // 피드 페이지 등 다른 페이지인 경우: 엑세스토큰 발급 요청 - console.log('🔑 피드 페이지: 엑세스토큰 발급 요청'); - console.log('📋 loginTokenKey:', loginTokenKey); + if (response.isSuccess) { + const { token } = response.data; - const response = await apiClient.post( - '/auth/exchange-temp-token', - { loginTokenKey }, - { withCredentials: true }, - ); + // 토큰을 localStorage에 저장 (request header에 사용) + localStorage.setItem('authToken', token); - if (response.data.isSuccess) { - console.log('✅ 엑세스토큰 발급 성공'); - // URL에서 loginTokenKey 파라미터 제거 - const newUrl = window.location.pathname; - window.history.replaceState({}, document.title, newUrl); - } else { - console.error('❌ 엑세스토큰 발급 실패:', response.data.message); - navigate('/'); - } + console.log('✅ Access 토큰 발급 성공 (바로 홈 화면)'); + + // URL에서 loginTokenKey 파라미터 제거 + const newUrl = window.location.pathname; + window.history.replaceState({}, document.title, newUrl); + } else { + console.error('❌ 토큰 발급 실패:', response.message); } } catch (error) { console.error('💥 토큰 발급 중 오류 발생:', error); - navigate('/'); } }; @@ -78,14 +53,14 @@ export const useSocialLoginToken = () => { // 토큰 발급 Promise를 저장 tokenPromise.current = handleSocialLoginToken(); } - }, [location.pathname, navigate]); + }, [location.pathname]); // 토큰 발급 완료를 기다리는 함수 반환 - const waitForToken = async (): Promise => { + const waitForToken = useCallback(async (): Promise => { if (tokenPromise.current) { await tokenPromise.current; } - }; + }, []); return { waitForToken }; }; diff --git a/src/pages/feed/Feed.tsx b/src/pages/feed/Feed.tsx index be82bf93..53b0df43 100644 --- a/src/pages/feed/Feed.tsx +++ b/src/pages/feed/Feed.tsx @@ -48,7 +48,7 @@ const Feed = () => { }; // 전체 피드 로드 함수 - const loadTotalFeeds = async (_cursor?: string) => { + const loadTotalFeeds = useCallback(async (_cursor?: string) => { try { setTotalLoading(true); @@ -74,10 +74,10 @@ const Feed = () => { } finally { setTotalLoading(false); } - }; + }, []); // 내 피드 로드 함수 - const loadMyFeeds = async (_cursor?: string) => { + const loadMyFeeds = useCallback(async (_cursor?: string) => { try { setMyLoading(true); const response = await getMyFeeds(_cursor ? { cursor: _cursor } : undefined); @@ -97,7 +97,7 @@ const Feed = () => { } finally { setMyLoading(false); } - }; + }, []); // 다음 페이지 로드 (무한 스크롤용) const loadMoreFeeds = useCallback(() => { @@ -150,6 +150,13 @@ const Feed = () => { // 토큰 발급 완료 대기 await waitForToken(); + // localStorage에 토큰이 있는지 확인 + const authToken = localStorage.getItem('authToken'); + if (!authToken) { + console.log('❌ 토큰이 없어서 피드를 로드할 수 없습니다.'); + return; + } + if (activeTab === '피드') { loadTotalFeeds(); } else if (activeTab === '내 피드') { @@ -158,7 +165,7 @@ const Feed = () => { }; loadFeedsWithToken(); - }, [activeTab, waitForToken]); + }, [activeTab, waitForToken, loadTotalFeeds, loadMyFeeds]); return ( diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx index 02835ce7..04075dcd 100644 --- a/src/pages/login/Login.tsx +++ b/src/pages/login/Login.tsx @@ -1,3 +1,5 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import styled from '@emotion/styled'; import logo from '../../assets/login/logo.svg'; import KaKao from '../../assets/login/kakao.svg'; @@ -5,6 +7,17 @@ import Google from '../../assets/login/google.svg'; import { Wrapper } from '@/components/common/Wrapper'; const Login = () => { + const navigate = useNavigate(); + + // 이미 토큰이 있으면 /feed로 바로 이동 + useEffect(() => { + const authToken = localStorage.getItem('authToken'); + if (authToken) { + console.log('✅ 이미 토큰이 있어서 /feed로 바로 이동합니다.'); + navigate('/feed'); + } + }, [navigate]); + const handleKakaoLogin = () => { // 직접 카카오 로그인 URL로 리다이렉션 window.location.href = `${import.meta.env.VITE_API_BASE_URL}/oauth2/authorization/kakao`; diff --git a/src/pages/signup/SignupGenre.tsx b/src/pages/signup/SignupGenre.tsx index cdee2fc6..6e9bfa16 100644 --- a/src/pages/signup/SignupGenre.tsx +++ b/src/pages/signup/SignupGenre.tsx @@ -61,25 +61,26 @@ const SignupGenre = () => { const handleNextClick = async () => { if (!selectedAlias || !nickname) return; - console.log('=== 🚀 다음 버튼 클릭 ==='); - console.log('🎭 선택된 alias:', selectedAlias); - console.log('👤 nickname:', nickname); - try { - console.log('🚀 postSignup API 호출 시작...'); - // ✅ 쿠키는 브라우저가 자동으로 전송 const result = await postSignup({ aliasName: selectedAlias.subTitle, nickname: nickname, isTokenRequired: false, }); - if (result.success) { + if (result.isSuccess) { console.log('🎉 회원가입 성공! 사용자 ID:', result.data.userId); + + // 회원가입 성공 시 새로운 access 토큰을 localStorage에 저장 + if (result.data.accessToken) { + localStorage.setItem('authToken', result.data.accessToken); + console.log('✅ 새로운 access 토큰이 localStorage에 저장되었습니다.'); + } + navigate('/signup/guide', { state: { - aliasName: selectedAlias.subTitle, nickname: nickname, + aliasName: selectedAlias.subTitle, }, }); } else { diff --git a/src/pages/signup/SignupNickname.tsx b/src/pages/signup/SignupNickname.tsx index 5a98f7b0..5066b628 100644 --- a/src/pages/signup/SignupNickname.tsx +++ b/src/pages/signup/SignupNickname.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Container, InputBox, StyledInput, CharCount } from './Signup.styled'; import Header from '../../components/common/TitleHeader'; @@ -12,17 +12,10 @@ const SignupNickname = () => { const navigate = useNavigate(); // 소셜 로그인 토큰 발급 처리 - useSocialLoginToken(); + const { waitForToken } = useSocialLoginToken(); const isNextActive = nickname.length >= 2 && nickname.length <= maxLength; - // 페이지 로드 시 간단한 확인 - useEffect(() => { - console.log('=== 🔍 SignupNickname 페이지 로드 ==='); - console.log('📍 현재 페이지:', window.location.pathname); - console.log('✅ 토큰 발급 후 쿠키는 브라우저가 자동으로 처리합니다.'); - }, []); - const handleBackClick = () => { navigate(-1); }; @@ -35,7 +28,18 @@ const SignupNickname = () => { console.log('👤 입력된 닉네임:', nickname); try { - // ✅ 쿠키는 브라우저가 자동으로 전송 + // 토큰 발급 완료 대기 + await waitForToken(); + + // localStorage에 토큰이 있는지 확인 + const authToken = localStorage.getItem('authToken'); + if (!authToken) { + console.log('❌ 토큰이 없어서 닉네임 검증을 할 수 없습니다.'); + setError('인증 토큰이 없습니다. 다시 시도해주세요.'); + return; + } + + console.log('✅ 토큰 확인 완료, 닉네임 검증 API 호출'); const result = await postNickname(nickname); if (result.data.isVerified) {