Skip to content
Merged
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
16 changes: 15 additions & 1 deletion src/entities/chat/model/socket.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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");
Expand Down
1 change: 1 addition & 0 deletions src/features/auth/ui/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const LoginForm = ({
}>("/auth/login", {
method: "POST",
body: JSON.stringify({ email, password }),
noAuth: true,
});

setAccessToken(res.accessToken);
Expand Down
1 change: 1 addition & 0 deletions src/features/auth/ui/SignUpForm/SignUpForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
34 changes: 23 additions & 11 deletions src/shared/api/fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,46 @@
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<T>(
endpoint: string,
options: RequestInit,
options: RequestInit & { noAuth?: boolean },
): Promise<T> {
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",
...restOptions,
});

//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,
});
}

Expand Down
34 changes: 28 additions & 6 deletions src/shared/api/refresh.ts
Original file line number Diff line number Diff line change
@@ -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<string | null> {
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;
}
}