diff --git a/src/entities/chat/model/socket.ts b/src/entities/chat/model/socket.ts index 48148638..702de261 100644 --- a/src/entities/chat/model/socket.ts +++ b/src/entities/chat/model/socket.ts @@ -1,5 +1,7 @@ +import { error } from "console"; import { MessageProps } from "./types"; import { useAuthStore } from "@/features/auth/model/auth.store"; +import { refreshAccessToken } from "@/shared/api/refresh"; export interface ChatSocketEvents { onOpen?: () => void; @@ -64,7 +66,13 @@ export class ChatSocket { } }; - this.socket.onclose = (event) => { + this.socket.onclose = async (event) => { + if (event.code === 4001) { + console.warn("[Socket] Token expired (4001)"); + + const newToken = await refreshAccessToken(); + this.reconnect(); + } console.warn("[Socket] Closed:", event.code); this.events.onClose?.(event.code, event.reason); this.socket = null; @@ -78,6 +86,12 @@ export class ChatSocket { }); } + private reconnect() { + console.log("[Socket] Reconnecting after refresh…"); + this.socket = null; + this.connect(); + } + sendMessage(type: "text" | "image", content: string) { if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { console.warn("[Socket] Not connected"); diff --git a/src/features/auth/ui/LoginForm/LoginForm.tsx b/src/features/auth/ui/LoginForm/LoginForm.tsx index 31772976..b2810fb6 100644 --- a/src/features/auth/ui/LoginForm/LoginForm.tsx +++ b/src/features/auth/ui/LoginForm/LoginForm.tsx @@ -39,6 +39,7 @@ export const LoginForm = ({ }>("/auth/login", { method: "POST", body: JSON.stringify({ email, password }), + noAuth: true, }); setAccessToken(res.accessToken); diff --git a/src/features/auth/ui/SignUpForm/SignUpForm.tsx b/src/features/auth/ui/SignUpForm/SignUpForm.tsx index b49bfab5..3a349e3c 100644 --- a/src/features/auth/ui/SignUpForm/SignUpForm.tsx +++ b/src/features/auth/ui/SignUpForm/SignUpForm.tsx @@ -135,6 +135,7 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => { const res = await apiFetch("/api/users/", { method: "POST", body: JSON.stringify(body), + noAuth: true, }); console.log("회원가입 성공:", res); diff --git a/src/shared/api/fetcher.ts b/src/shared/api/fetcher.ts index ca387a0b..47632c48 100644 --- a/src/shared/api/fetcher.ts +++ b/src/shared/api/fetcher.ts @@ -1,19 +1,26 @@ +import { useAuthStore } from "@/features/auth/model/auth.store"; +import { refreshAccessToken } from "./refresh"; import { useModalStore } from "@/shared/model/modal.store"; const BASE_URL = process.env.NEXT_PUBLIC_API_URL; export async function apiFetch( endpoint: string, - options: RequestInit, + options: RequestInit & { noAuth?: boolean }, ): Promise { - const { headers, ...restOptions } = options; + const { headers, noAuth, ...restOptions } = options; + const { accessToken, setAccessToken, logout } = useAuthStore.getState(); const { openModal, closeModal } = useModalStore.getState(); const defaultHeaders: HeadersInit = { "Content-Type": "application/json", }; - const res = await fetch(`${BASE_URL}${endpoint}`, { + if (!noAuth && typeof window !== "undefined") { + defaultHeaders["Authorization"] = `Bearer ${accessToken}`; + } + + let res = await fetch(`${BASE_URL}${endpoint}`, { headers: { ...defaultHeaders, ...headers }, cache: "no-store", credentials: "include", @@ -21,14 +28,19 @@ export async function apiFetch( }); //AccessToken 만료 처리 - if (res.status === 401) { - openModal("normal", { - message: "세션이 만료되었습니다. 다시 로그인 해주세요.", - buttonText: "확인", - onClick: () => { - closeModal(); - location.replace("/login"); - }, + if (res.status === 401 && !noAuth) { + const newToken = await refreshAccessToken(); + + if (!newToken) throw new Error("세션 만료"); + + defaultHeaders["Authorization"] = `Bearer ${newToken}`; + + //동일한 경로에 요청 재시도 + res = await fetch(`${BASE_URL}${endpoint}`, { + headers: { ...defaultHeaders, ...headers }, + cache: "no-store", + credentials: "include", + ...restOptions, }); } diff --git a/src/shared/api/refresh.ts b/src/shared/api/refresh.ts index 804caa5b..4e3187b7 100644 --- a/src/shared/api/refresh.ts +++ b/src/shared/api/refresh.ts @@ -1,26 +1,48 @@ import { useAuthStore } from "@/features/auth/model/auth.store"; +import { useModalStore } from "@/shared/model/modal.store"; const BASE_URL = process.env.NEXT_PUBLIC_API_URL; export async function refreshAccessToken(): Promise { + const { setAccessToken, logout } = useAuthStore.getState(); + const { openModal, closeModal } = useModalStore.getState(); + try { const res = await fetch(`${BASE_URL}/auth/refresh`, { method: "POST", - credentials: "include", // 쿠키 전송 + credentials: "include", }); if (!res.ok) { + logout(); + openModal("normal", { + message: "세션이 만료되었습니다. 다시 로그인 해주세요.", + buttonText: "확인", + onClick: () => { + closeModal(); + location.replace("/login"); + }, + }); return null; } const data = await res.json(); - const newAccessToken = data.accessToken as string; + const newToken = data.accessToken as string; + + setAccessToken(newToken); - // zustand 업데이트 - useAuthStore.getState().setAccessToken(newAccessToken); + return newToken; + } catch (err) { + logout(); + openModal("normal", { + message: "세션이 만료되었습니다. 다시 로그인 해주세요.", + buttonText: "확인", + onClick: () => { + closeModal(); + location.replace("/login"); + }, + }); - return newAccessToken; - } catch { return null; } }