Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.example
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=

1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
content="자리 자체를 예약하는 새로운 식문화 플랫폼 웹 애플리케이션 서비스"
/>
<meta property="og:image" content="/eatsfineLogo.svg" />
<script src="https://developers.kakao.com/sdk/js/kakao.js"></script>
</head>
<body>
<div id="root"></div>
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,22 @@
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@react-oauth/google": "^0.13.4",
"@tanstack/react-query": "^5.90.16",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"immer": "^11.1.3",
"lucide-react": "^0.562.0",
"react": "^19.2.0",
"react-day-picker": "^9.13.0",
"react-dom": "^19.2.0",
"react-hook-form": "^7.69.0",
"react-router-dom": "^7.11.0",
"tailwind-merge": "^3.4.0",
"zod": "^4.3.4"
"zod": "^4.3.4",
"zustand": "^5.0.11"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
Expand Down
49 changes: 49 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ import SubscriptionPage from "./pages/myPage/subscriptionPage";
import ReservationPage from "./pages/myPage/reservationPage";
import StorePage from "./pages/myPage/storePage";
import OwnerPage from "./pages/ownerPage";
import { useEffect } from "react";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/App.tsx | head -100

Repository: Eatsfine/FE

Length of output: 3175


🏁 Script executed:

# Search for Kakao SDK script loading
rg -i "kakao" --type html --type js --type ts --type jsx --type tsx -A 3 -B 3 | head -100

Repository: Eatsfine/FE

Length of output: 82


🏁 Script executed:

# Search for Kakao SDK script loading without type restrictions
rg -i "kakao" -A 3 -B 3 --max-count 50

Repository: Eatsfine/FE

Length of output: 19669


🏁 Script executed:

cat -n index.html

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
In `@src/App.tsx` at line 22, Add a retry/backoff initialization inside the App
component's useEffect so the Kakao SDK init is attempted multiple times if
window.Kakao isn't ready yet: inside useEffect in App, poll or retry (e.g.,
exponential backoff or fixed intervals, max attempts) to check for window.Kakao
and call window.Kakao.init(...) once available, and ensure you clear
timers/intervals on unmount; keep existing defensive checks in LoginDialog and
SignupDialog but move SDK-ready guarantees to App's useEffect to reduce missed
initializations.


const routes: RouteObject[] = [
{
//TODO: 로그아웃처리 필요
element: <PublicLayout onLogOut={() => {}} />,
element: <PublicLayout />,
errorElement: <NotFound />,
children: [
{ path: "/search", element: <SearchPage /> },
Expand Down Expand Up @@ -72,6 +72,13 @@ const routes: RouteObject[] = [
const router = createBrowserRouter(routes);

export default function App() {
// 카카오 sdk 초기화
useEffect(() => {
if (window.Kakao && !window.Kakao.isInitialized()) {
window.Kakao.init(import.meta.env.VITE_KAKAO_JS_KEY);
}
}, []);

return (
<>
<RouterProvider router={router} />
Expand Down
69 changes: 69 additions & 0 deletions src/api/auth.ts
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

localStorage 키 불일치 버그

clearAuth()"accessToken" 키를 삭제하지만, useAuthStore"auth-storage" 키에 {accessToken, isAuthenticated}를 저장합니다. 실제로 인증 상태가 클리어되지 않는 버그예요.

🐛 수정 제안
 export const clearAuth = () => {
-  localStorage.removeItem("accessToken");
+  localStorage.removeItem("auth-storage");
   window.location.href = "/";
 };

또는 Zustand store의 logout 액션을 직접 호출하는 방식도 고려해볼 수 있어요:

import { useAuthStore } from "@/stores/useAuthStore";

export const clearAuth = () => {
  useAuthStore.getState().actions.logout();
  window.location.href = "/";
};

As per coding guidelines: 에러 처리/타임아웃/리트라이 전략 확인, 응답 타입 안전성 유지.

🤖 Prompt for AI Agents
In `@src/api/auth.ts` around lines 50 - 53, clearAuth currently removes
"accessToken" but the app stores auth under "auth-storage", so update clearAuth
to clear the actual storage entry or, better, call the Zustand logout action to
ensure state and storage are cleared: use
useAuthStore.getState().actions.logout() (or remove the "auth-storage" key and
any nested {accessToken,isAuthenticated} values) and then navigate to "/"; keep
the change focused in clearAuth and ensure the logout call or manual removal
preserves response/type safety and any existing error handling/timeouts in the
logout flow.


export const logout = async () => {
try {
await postLogout();
} catch (e) {
console.error(e);
} finally {
clearAuth();
}
};
Comment on lines +55 to +63
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check logout function usage across the codebase
rg -n "logout\(" --type=ts --type=tsx -B2 -A2 | head -60

Repository: Eatsfine/FE

Length of output: 82


🏁 Script executed:

# Check imports of auth functions
rg -n "from.*auth|import.*logout" --type=ts --type=tsx | head -30

Repository: 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 logout() 함수의 에러 처리 전략을 명확히 해야 합니다

현재 코드를 보니 실제로 두 가지 로그아웃 로직이 섞여 있네요:

  • PublicLayout.tsx: 스토어의 logout() 호출 (클라이언트 상태만 클리어)
  • src/api/auth.ts: logout() 함수 (서버 호출 + 클라이언트 클리어)

더 큰 문제는 API 함수의 에러 처리인데, 현재는 서버 호출 실패 시에도 clearAuth()가 무조건 실행돼서 클라이언트와 서버 상태가 불일치할 수 있어요.

export const logout = async () => {
  try {
    await postLogout();
  } catch (e) {
    console.error(e);  // ← 실패 후에도 토큰을 지워버림
  } finally {
    clearAuth();  // ← 항상 실행
  }
};

제안:

  1. 로그아웃 사용처 통일 → PublicLayout에서도 API logout()을 호출하도록 변경
  2. 에러 발생 시 사용자에게 알리고, 재시도 기회 제공
  3. 필요하면 타임아웃/재시도 로직 추가
export const logout = async () => {
  try {
    await postLogout();
    clearAuth();  // 성공 시에만 클리어
  } catch (error) {
    console.error("로그아웃 실패:", error);
    throw error;  // 호출처에서 처리하도록
  }
};
🤖 Prompt for AI Agents
In `@src/api/auth.ts` around lines 55 - 63, The logout API currently always clears
client auth state even when postLogout() fails; update the export const logout
function to call await postLogout() and only call clearAuth() on success, log
the error with context (e.g., "로그아웃 실패:"), then rethrow the error so callers can
show a user-facing notification/retry UI; also change callers (e.g.,
PublicLayout.tsx) to invoke this API logout instead of the store-only logout to
keep client/server state consistent and consider adding optional timeout/retry
logic around postLogout().


export const postRefresh = async () => {
const { data } =
await api.post<ApiResponse<ResponseRefreshDto>>("/auth/refresh");
return data;
};
42 changes: 39 additions & 3 deletions src/api/axios.ts
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";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find src/api -type f -name "*.ts" | head -20

Repository: Eatsfine/FE

Length of output: 167


🏁 Script executed:

cat -n src/api/axios.ts

Repository: Eatsfine/FE

Length of output: 3117


🏁 Script executed:

cat -n src/api/auth.ts | head -50

Repository: Eatsfine/FE

Length of output: 1582


동시 401 요청에서 토큰 재발급이 중복 호출되고 있어요

현재 코드에서 여러 요청이 동시에 401을 받으면 각각 postRefresh()를 독립적으로 호출하게 되어 경쟁 상태가 발생합니다. 예를 들어, 요청 A와 B가 동시에 401을 받으면 둘 다 새로운 토큰 재발급을 시작하는데, 이때 한 요청의 실패가 clearAuth()를 호출하면서 다른 요청은 토큰을 잃게 되는 문제가 생겨요.

또한 postRefresh()가 성공했지만 response.success === false를 반환하는 경우에 대한 명시적 처리가 없어서, 에러 상황이 제대로 전파되지 않을 수 있습니다.

🛠️ 해결 방안
+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
In `@src/api/axios.ts` at line 4, Multiple concurrent 401s cause duplicate token
refreshes and race conditions; change the logic around postRefresh/clearAuth so
only one refresh runs at a time by introducing a shared in-flight promise (e.g.,
refreshingTokenPromise) that other requests await instead of calling postRefresh
again, have the single refresh handler check postRefresh() result and throw if
response.success === false so callers see the failure, and ensure clearAuth() is
only called after the refresh promise finally rejects (or when a non-recoverable
error occurs) so other waiting requests don’t lose tokens mid-refresh; update
the axios response interceptor (where postRefresh and clearAuth are used) to use
this shared promise and propagate errors to callers.


export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL as string | undefined,
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down
Loading