-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 로그인 / 회원가입 API 연동 #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
99cfb07
b784c20
a2b3ced
22de449
31db01a
b5944e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,6 @@ | ||
| VITE_API_URL=https://api.example.com | ||
|
|
||
| VITE_KAKAO_JS_KEY= | ||
| VITE_KAKAO_JS_KEY= | ||
|
|
||
| VITE_GOOGLE_CLIENT_ID= | ||
|
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import type { ApiResponse } from "@/types/api"; | ||
| import type { | ||
| RequestLoginDto, | ||
| RequestSignupDto, | ||
| RequestSocialLoginDto, | ||
| ResponseLoginDto, | ||
| ResponseLogoutDto, | ||
| ResponseRefreshDto, | ||
| ResponseSignupDto, | ||
| } from "@/types/auth"; | ||
| import { api } from "./axios"; | ||
|
|
||
| export const postSignup = async ( | ||
| body: RequestSignupDto, | ||
| ): Promise<ApiResponse<ResponseSignupDto>> => { | ||
| const { data } = await api.post<ApiResponse<ResponseSignupDto>>( | ||
| "/auth/signup", | ||
| body, | ||
| ); | ||
| return data; | ||
| }; | ||
|
|
||
| export const postLogin = async ( | ||
| body: RequestLoginDto, | ||
| ): Promise<ApiResponse<ResponseLoginDto>> => { | ||
| const { data } = await api.post<ApiResponse<ResponseLoginDto>>( | ||
| "/auth/login", | ||
| body, | ||
| ); | ||
| return data; | ||
| }; | ||
|
|
||
| export const postSocialLogin = async ( | ||
| provider: "kakao" | "google", | ||
| body: RequestSocialLoginDto, | ||
| ): Promise<ApiResponse<ResponseLoginDto>> => { | ||
| const { data } = await api.post<ApiResponse<ResponseLoginDto>>( | ||
| `/auth/social/${provider}`, | ||
| body, | ||
| ); | ||
| return data; | ||
| }; | ||
|
|
||
| export const postLogout = async () => { | ||
| const { data } = | ||
| await api.post<ApiResponse<ResponseLogoutDto>>("/auth/logout"); | ||
| return data; | ||
| }; | ||
|
|
||
| export const clearAuth = () => { | ||
| localStorage.removeItem("accessToken"); | ||
| window.location.href = "/"; | ||
| }; | ||
|
Comment on lines
+50
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. localStorage 키 불일치 버그
🐛 수정 제안 export const clearAuth = () => {
- localStorage.removeItem("accessToken");
+ localStorage.removeItem("auth-storage");
window.location.href = "/";
};또는 Zustand store의 import { useAuthStore } from "@/stores/useAuthStore";
export const clearAuth = () => {
useAuthStore.getState().actions.logout();
window.location.href = "/";
};As per coding guidelines: 에러 처리/타임아웃/리트라이 전략 확인, 응답 타입 안전성 유지. 🤖 Prompt for AI Agents |
||
|
|
||
| export const logout = async () => { | ||
| try { | ||
| await postLogout(); | ||
| } catch (e) { | ||
| console.error(e); | ||
| } finally { | ||
| clearAuth(); | ||
| } | ||
| }; | ||
|
Comment on lines
+55
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Check logout function usage across the codebase
rg -n "logout\(" --type=ts --type=tsx -B2 -A2 | head -60Repository: Eatsfine/FE Length of output: 82 🏁 Script executed: # Check imports of auth functions
rg -n "from.*auth|import.*logout" --type=ts --type=tsx | head -30Repository: Eatsfine/FE Length of output: 82 🏁 Script executed: # Check the actual auth.ts file to see all logout-related functions
fd auth.ts -t f --exec cat -n {} \;Repository: Eatsfine/FE Length of output: 10070 🏁 Script executed: # Look for PublicLayout and its logout usage
fd PublicLayout.tsx -t f --exec cat -n {} \;Repository: Eatsfine/FE Length of output: 1965 API 현재 코드를 보니 실제로 두 가지 로그아웃 로직이 섞여 있네요:
더 큰 문제는 API 함수의 에러 처리인데, 현재는 서버 호출 실패 시에도 export const logout = async () => {
try {
await postLogout();
} catch (e) {
console.error(e); // ← 실패 후에도 토큰을 지워버림
} finally {
clearAuth(); // ← 항상 실행
}
};제안:
export const logout = async () => {
try {
await postLogout();
clearAuth(); // 성공 시에만 클리어
} catch (error) {
console.error("로그아웃 실패:", error);
throw error; // 호출처에서 처리하도록
}
};🤖 Prompt for AI Agents |
||
|
|
||
| export const postRefresh = async () => { | ||
| const { data } = | ||
| await api.post<ApiResponse<ResponseRefreshDto>>("/auth/refresh"); | ||
| return data; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import axios, { type AxiosError, type InternalAxiosRequestConfig } from "axios"; | ||
| import { isApiResponse, normalizeApiError } from "./api.error"; | ||
| import type { ApiError } from "@/types/api"; | ||
| import { clearAuth, postRefresh } from "./auth"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find src/api -type f -name "*.ts" | head -20Repository: Eatsfine/FE Length of output: 167 🏁 Script executed: cat -n src/api/axios.tsRepository: Eatsfine/FE Length of output: 3117 🏁 Script executed: cat -n src/api/auth.ts | head -50Repository: Eatsfine/FE Length of output: 1582 동시 401 요청에서 토큰 재발급이 중복 호출되고 있어요 현재 코드에서 여러 요청이 동시에 401을 받으면 각각 또한 🛠️ 해결 방안+let refreshPromise: ReturnType<typeof postRefresh> | null = null;
+
api.interceptors.response.use(
(res) => {
const data = res.data;
@@
try {
- //토큰 재발급 요청
- const response = await postRefresh();
+ // 이미 진행 중인 재발급이 있으면 그 결과 재사용
+ if (!refreshPromise) {
+ refreshPromise = postRefresh().finally(() => {
+ refreshPromise = null;
+ });
+ }
+ const response = await refreshPromise;
+
+ // 재발급은 성공했지만 서버에서 실패 응답을 준 경우
+ if (!response.success) {
+ clearAuth();
+ return Promise.reject(apiError);
+ }
if (response.success) {
const newAccessToken = response.data.accessToken;🤖 Prompt for AI Agents |
||
|
|
||
| export const api = axios.create({ | ||
| baseURL: import.meta.env.VITE_API_URL as string | undefined, | ||
|
|
@@ -35,7 +36,10 @@ api.interceptors.response.use( | |
| return res; | ||
| }, | ||
|
|
||
| (err: AxiosError) => { | ||
| async (err: AxiosError) => { | ||
| const originalRequest = err.config as InternalAxiosRequestConfig & { | ||
| _retry?: boolean; | ||
| }; | ||
| const apiError: ApiError = normalizeApiError(err); | ||
|
|
||
| if (import.meta.env.DEV) { | ||
|
|
@@ -48,8 +52,40 @@ api.interceptors.response.use( | |
| }); | ||
| } | ||
|
|
||
| if (apiError.status === 401) { | ||
| // TODO: refresh or logout 정책 확정 후 구현 | ||
| if (apiError.status === 401 && originalRequest) { | ||
| // 이미 재시도한 요청이거나, 재발급 요청 자체가 실패인 경우 -> 로그아웃 | ||
| if ( | ||
| originalRequest._retry || | ||
| originalRequest.url?.includes("/auth/refresh") | ||
| ) { | ||
| clearAuth(); | ||
| return Promise.reject(apiError); | ||
| } | ||
|
|
||
| originalRequest._retry = true; // 재시도 플래그 설정 | ||
|
|
||
| try { | ||
| //토큰 재발급 요청 | ||
| const response = await postRefresh(); | ||
|
|
||
| if (response.success) { | ||
| const newAccessToken = response.data.accessToken; | ||
|
|
||
| // 새 토큰 저장 | ||
| localStorage.setItem("accessToken", newAccessToken); | ||
|
|
||
| // 실패했던 요청의 헤더를 새 토큰으로 교체 | ||
| originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; | ||
|
|
||
| // 재시도 | ||
| return api(originalRequest); | ||
| } | ||
| } catch (refreshError) { | ||
| // 재발급 실패 시 로그아웃 처리 | ||
| console.error("토큰 재발급 실패:", refreshError); | ||
| clearAuth(); | ||
| return Promise.reject(refreshError); | ||
| } | ||
| } | ||
|
|
||
| return Promise.reject(apiError); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n src/App.tsx | head -100Repository: Eatsfine/FE
Length of output: 3175
🏁 Script executed:
Repository: Eatsfine/FE
Length of output: 82
🏁 Script executed:
Repository: Eatsfine/FE
Length of output: 19669
🏁 Script executed:
Repository: Eatsfine/FE
Length of output: 945
카카오 SDK 초기화 시 재시도 로직 추가 권장
현재 코드는 useEffect가 한 번만 실행되므로, 외부 CDN에서 SDK가 로드되는 시간이 지연되는 경우(네트워크 지연 등) 초기화가 건너뛰어질 수 있어요. 특히 로그인 기능이 필요한 순간에 SDK를 사용할 수 없을 수 있습니다.
LoginDialog와 SignupDialog에서 이미 방어 로직이 있지만, App 레벨에서도 재시도 로직을 추가하면 더 안정적이에요.
💡 개선 예시
useEffect(() => { - if (window.Kakao && !window.Kakao.isInitialized()) { - window.Kakao.init(import.meta.env.VITE_KAKAO_JS_KEY); - } + const tryInit = () => { + if (window.Kakao && !window.Kakao.isInitialized()) { + window.Kakao.init(import.meta.env.VITE_KAKAO_JS_KEY); + return true; + } + return false; + }; + + if (tryInit()) return; + const interval = setInterval(() => { + if (tryInit()) clearInterval(interval); + }, 200); + return () => clearInterval(interval); }, []);🤖 Prompt for AI Agents