diff --git a/src/api/refresh.ts b/src/api/refresh.ts index 11af1b3..949c35e 100644 --- a/src/api/refresh.ts +++ b/src/api/refresh.ts @@ -1,35 +1,34 @@ -import type { refreshDto } from "../types/user"; -import { refreshAxiosInstance } from "./axios"; +import type { refreshDto } from '../types/user'; +import { refreshAxiosInstance } from './axios'; let timeout: number | null = null; -function getExp(accessToken: string) { +export function getExp(accessToken: string) { try { - const [, p] = accessToken.split("."); + const [, p] = accessToken.split('.'); if (!p) return null; - const b64 = p.replace(/-/g, "+").replace(/_/g, "/"); - const padded = b64.padEnd(b64.length + ((4 - (b64.length % 4)) % 4), "="); + const b64 = p.replace(/-/g, '+').replace(/_/g, '/'); + const padded = b64.padEnd(b64.length + ((4 - (b64.length % 4)) % 4), '='); const { exp } = JSON.parse(atob(padded)); - return typeof exp === "number" ? exp : null; + return typeof exp === 'number' ? exp : null; } catch { return null; } } -const patchSignUp = async (): Promise => { - const { data } = await refreshAxiosInstance.post("/auth/reissue"); - localStorage.setItem("accessToken", data.result.accessToken); +export const patchSignUp = async (): Promise => { + const { data } = await refreshAxiosInstance.post('/auth/reissue'); + localStorage.setItem('accessToken', data.result.accessToken); return data; }; export async function refreshTimer(bufferSec = 60): Promise { - if (timeout) { window.clearTimeout(timeout); timeout = null; } - const accessToken = localStorage.getItem("accessToken"); + const accessToken = localStorage.getItem('accessToken'); if (!accessToken) return; const exp = getExp(accessToken); @@ -42,10 +41,12 @@ export async function refreshTimer(bufferSec = 60): Promise { try { await patchSignUp(); } catch (error) { - console.error("재발급 실패", error); + console.error('재발급 실패', error); } finally { // 1초 후 새 토큰 기준으로 다시 '한 번'만 예약 - timeout = window.setTimeout(() => { void refreshTimer(bufferSec); }, 1000); + timeout = window.setTimeout(() => { + void refreshTimer(bufferSec); + }, 1000); } }; diff --git a/src/hooks/chat/useWebSocket.ts b/src/hooks/chat/useWebSocket.ts index 3abc49c..3e52421 100644 --- a/src/hooks/chat/useWebSocket.ts +++ b/src/hooks/chat/useWebSocket.ts @@ -2,6 +2,7 @@ import { Client, type IFrame, type IMessage } from '@stomp/stompjs'; import { useEffect, useRef, useState } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import type { ChatMessage } from '../../types/chat'; +import { getExp, patchSignUp } from '../../api/refresh'; export const webSocketStatus = { CONNECTING: 'CONNECTING', @@ -30,12 +31,49 @@ export const useWebSocket = (roomId: number) => { setStatus('CONNECTING'); + let token = localStorage.getItem('accessToken'); + + const refreshAccessToken = async () => { + try { + const { result } = await patchSignUp(); // 토큰 갱신 API 호출 + if (!result?.accessToken) { + throw new Error('No access token returned'); + } + return result.accessToken; + } catch (err) { + console.error('Token refresh failed:', err); + return null; + } + }; + + if (token) { + const exp = getExp(token); + // 토큰이 만료되었거나 1분 이내 만료 시 재발급 + if (!exp || exp * 1000 - 60000 < Date.now()) { + console.log('Access token is expired or expiring soon, refreshing for WebSocket...'); + const newAccessToken = refreshAccessToken(); + if (newAccessToken) { + localStorage.setItem('accessToken', String(newAccessToken)); // 갱신된 토큰 저장 + token = localStorage.getItem('accessToken'); + } else { + console.error('Failed to refresh token for WebSocket. Connection aborted.'); + setStatus('CLOSED'); + return; // 토큰 갱신 실패 시 연결 중단 + } + } + } + + if (!token) { + console.error('No valid access token for WebSocket connection.'); + return; + } + // stomp 클라이언트 객체 생성 const client = new Client({ webSocketFactory: () => new WebSocket(import.meta.env.VITE_WS_URL), // 헤더에 토큰 전달 connectHeaders: { - Authorization: `Bearer ${localStorage.getItem('accessToken')}`, + Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, reconnectDelay: 5000, // 자동 재연결 시도 간격