diff --git a/src/api/client.ts b/src/api/client.ts index 7fc321a..025241e 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -12,7 +12,10 @@ import axios from "axios"; // const BASE_URL = "http://localhost:8080/api"; export const BASE_URL = "https://api.serverway.shop/api"; -// 메모리에 토큰 저장 +/** + * Access Token을 메모리에 저장하기 위한 변수 (새로고침 시 초기화됨) + * HttpOnly 쿠키로 Refresh Token을 관리하는 구조이므로 Access Token만 메모리 관리 + */ let accessTokenInMemory: string | null = null; /** @@ -41,10 +44,13 @@ const client = axios.create({ "Content-Type": "application/json", Accept: "application/json", }, - withCredentials: true, // HttpOnly 쿠키 전송 (Refresh Token) + withCredentials: true, // Refresh Token(HttpOnly Cookie) 자동 전송 }); -// 요청 인터셉터 +/** + * 요청 인터셉터 + * - In-Memory Access Token이 존재하면 Authorization 헤더 자동 추가 + */ client.interceptors.request.use( (config) => { if (accessTokenInMemory) { @@ -55,17 +61,25 @@ client.interceptors.request.use( (error) => Promise.reject(error) ); -// 응답 인터셉터 +/** + * 응답 인터셉터 + * + * - 401(Unauthorized) 발생 시 Refresh Token을 사용해 토큰 재발급 시도 + * - 재발급 성공 시 자동으로 원래 요청 재시도 + * - 실패 시 Access Token 삭제 및 메인 페이지로 리다이렉트 + */ client.interceptors.response.use( (response) => response, + async (error) => { const originalRequest = error.config; + // 401 + 재시도한 적 없는 요청만 처리 if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { - // Refresh Token은 HttpOnly 쿠키로 자동 전송됨 + // Refresh Token은 HttpOnly 쿠키로 자동 포함됨 const response = await axios.post( `${BASE_URL}/auth/refresh`, {}, @@ -73,17 +87,25 @@ client.interceptors.response.use( ); const newToken = response.data.accessToken; - setAccessToken(newToken); // 메모리에 저장 + // 새 Access Token 메모리에 저장 + setAccessToken(newToken); + + // 원래 요청에 새로운 토큰 적용 originalRequest.headers["Authorization"] = `Bearer ${newToken}`; + return client(originalRequest); } catch (error) { console.error("Token refresh failed:", error); + + // 토큰 삭제 및 로그아웃 처리 setAccessToken(null); window.location.href = "/"; + return Promise.reject(error); } } + return Promise.reject(error); } ); diff --git a/src/domains/login/api/loginApi.ts b/src/domains/login/api/loginApi.ts index 4e8c6a2..ddae8cf 100644 --- a/src/domains/login/api/loginApi.ts +++ b/src/domains/login/api/loginApi.ts @@ -1,5 +1,5 @@ /** - * @author 김대호 + * @author 김대호 구희원 * @description 로그인 API - 사용자 인증을 처리하는 API 호출 * 아이디와 비밀번호를 서버로 전송하여 인증 토큰 및 사용자 정보 반환 * 응답은 API 표준 형식(status_code, status_message, result)을 따름 @@ -8,6 +8,9 @@ import client from "@api/client"; import type { LoginRequest, LoginResponse } from "../types/login"; +/** + * API 응답 타입 정의 + */ interface LoginApiResponse { status_code: number; status_message: string; @@ -22,6 +25,9 @@ interface LoginApiResponse { */ export const loginApi = async (data: LoginRequest): Promise => { console.log("Using client:", client.defaults.withCredentials); + const response = await client.post("/auth/login", data); + + // API의 result 필드만 반환 return response.data.result; }; diff --git a/src/domains/login/api/refreshApi.ts b/src/domains/login/api/refreshApi.ts index f179ade..1ee2418 100644 --- a/src/domains/login/api/refreshApi.ts +++ b/src/domains/login/api/refreshApi.ts @@ -1,5 +1,20 @@ +/** + * @author 김대호 + * @description Access Token 갱신 API 호출 유틸리티 + * + * - 서버에 Refresh Token 기반 요청 + * - 새 Access Token을 반환 + */ + import client from "@api/client"; +/** + * @interface RefreshApiResponse + * @description Refresh API 응답 타입 + * @property {number} status_code - 상태 코드 + * @property {string} status_message - 상태 메시지 + * @property {{ accessToken: string }} result - 새로 발급된 Access Token + */ interface RefreshApiResponse { status_code: number; status_message: string; @@ -8,6 +23,11 @@ interface RefreshApiResponse { }; } +/** + * @function refreshTokenApi + * @description Refresh Token으로 새로운 Access Token 발급 + * @returns {Promise} - 새 Access Token + */ export const refreshTokenApi = async (): Promise => { const response = await client.post("/auth/refresh"); return response.data.result.accessToken; diff --git a/src/domains/login/components/AnimationBackground.tsx b/src/domains/login/components/AnimationBackground.tsx index ff98705..f141719 100644 --- a/src/domains/login/components/AnimationBackground.tsx +++ b/src/domains/login/components/AnimationBackground.tsx @@ -1,6 +1,18 @@ +/** + * @author 구희원 + * @description 배경 애니메이션용 Circle 컴포넌트 모음 + * + * - 여러 Circle 컴포넌트를 지정된 위치, 크기, 색상으로 배치 + * - Tailwind CSS를 활용한 위치 및 z-index 관리 + * - 화면 전체를 덮는 고정 배경으로 설정 + */ + import Circle from "./Circle"; import type { CircleConfig } from "../types/circle"; +/** + * 각 Circle의 설정 정보 + */ const CIRCLE_CONFIGS: CircleConfig[] = [ { id: "1", size: "small", gradient: "blue", position: "top-1/3 left-1/12" }, { @@ -10,27 +22,20 @@ const CIRCLE_CONFIGS: CircleConfig[] = [ position: "bottom-1/4 left-1/12", }, { id: "3", size: "medium", gradient: "pink", position: "top-1/4 left-1/3" }, - { - id: "4", - size: "medium", - gradient: "orange", - position: "top-1/2 left-1/3", - }, + { id: "4", size: "medium", gradient: "orange", position: "top-1/2 left-1/3" }, { id: "5", size: "small", gradient: "blue", position: "top-1/2 right-1/4" }, - { - id: "6", - size: "small", - gradient: "purple", - position: "top-1/2 right-1/7", - }, - { - id: "7", - size: "small", - gradient: "pink", - position: "top-1/6 right-1/12", - }, + { id: "6", size: "small", gradient: "purple", position: "top-1/2 right-1/7" }, + { id: "7", size: "small", gradient: "pink", position: "top-1/6 right-1/12" }, ]; +/** + * 배경 애니메이션 컴포넌트 + * + * @returns {JSX.Element} 배경에 고정된 Circle 애니메이션 요소 + * + * @example + * + */ function AnimationBackground() { return (
diff --git a/src/domains/login/components/Button.tsx b/src/domains/login/components/Button.tsx index 7feb724..180a590 100644 --- a/src/domains/login/components/Button.tsx +++ b/src/domains/login/components/Button.tsx @@ -1,9 +1,29 @@ +/** + * @author 구희원 + * @description 재사용 가능한 버튼 컴포넌트 + * + * - 텍스트와 disabled 상태를 받아 버튼 생성 + * - Tailwind CSS를 활용한 스타일링 + * - 기본 type="submit" + */ + interface ButtonProps { + /** 버튼에 표시될 텍스트 */ text: string; + /** 버튼 활성화 여부 (기본값: false) */ disabled?: boolean; } +/** + * 버튼 컴포넌트 + * + * @param {ButtonProps} props - 버튼 속성 + * @returns {JSX.Element} 스타일이 적용된 버튼 요소 + * + * @example + *