diff --git a/package.json b/package.json index abfbe9b7..69b30d0d 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "@mui/icons-material": "^5.16.0", "@mui/material": "^5.16.0", "@mui/x-date-pickers": "^7.9.0", + "@tanstack/react-query": "^5.51.1", + "@tanstack/react-query-devtools": "^5.51.1", "axios": "^1.7.2", "dayjs": "^1.11.11", "jotai": "^2.8.4", diff --git a/src/App.tsx b/src/App.tsx index d45dd72b..4b3993da 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,8 @@ import { ThemeProvider } from "styled-components"; import Modal from "@components/commons/modal/Modal"; import { ThemeProvider as MuiThemeProvider, createTheme } from "@mui/material/styles"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; const darkTheme = createTheme({ palette: { @@ -18,18 +20,25 @@ const darkTheme = createTheme({ }); function App() { + const queryClient = new QueryClient(); + return ( - - - - - - - - - - - + + + + + + + + + + + + +
+ +
+
); } diff --git a/src/apis/.gitkeep b/src/apis/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/apis/domains/bookings/api.ts b/src/apis/domains/bookings/api.ts new file mode 100644 index 00000000..8607e931 --- /dev/null +++ b/src/apis/domains/bookings/api.ts @@ -0,0 +1,47 @@ +import { get, post } from "@apis/index"; +import { components } from "@typings/api/schema"; +import { ApiResponseType } from "@typings/commonType"; +import { AxiosResponse } from "axios"; + +export interface postGuestReq { + scheduleId: number; + purchaseTicketCount: number; + scheduleNumber: string; + bookerName: string; + bookerPhoneNumber: string; + birthDate: string; + password: string; + totalPaymentAmount: number; + isPaymentCompleted: boolean; +} + +type GuestBookingResponse = components["schemas"]["GuestBookingResponse"]; + +// 1. API 요청 함수 작성 및 타입 추가 +export const postGuestBook = async ( + formData: postGuestReq +): Promise => { + try { + const response: AxiosResponse> = await post( + "/bookings/guest", + formData + ); + + return response.data.data; + } catch (error) { + console.error("error", error); + + return null; + } +}; + +export const getGuestBookingList = async () => { + try { + const response = await get("/bookings/guest/retrieve"); + + console.log(response.data); + return response; + } catch (error) { + console.error("error", error); + } +}; diff --git a/src/apis/domains/bookings/queries.ts b/src/apis/domains/bookings/queries.ts new file mode 100644 index 00000000..43244960 --- /dev/null +++ b/src/apis/domains/bookings/queries.ts @@ -0,0 +1,28 @@ +import { QueryClient, useMutation, useQuery } from "@tanstack/react-query"; +import { getGuestBookingList, postGuestBook, postGuestReq } from "./api"; + +export const QUERY_KEY = { + LIST: "list", +}; + +// 2. 쿼리 작성 +export const useGuestBook = () => { + const queryClient = new QueryClient(); + + return useMutation({ + mutationFn: (formData: postGuestReq) => postGuestBook(formData), // API 요청 함수 + onSuccess: (res) => { + // 성공 시, 호출 + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.LIST] }); + }, + }); +}; + +export const useGetGuestBookingList = () => { + return useQuery({ + queryKey: [QUERY_KEY.LIST], + queryFn: () => getGuestBookingList(), + staleTime: 1000 * 60 * 60, + gcTime: 1000 * 60 * 60 * 24, + }); +}; diff --git a/src/apis/domains/home/api.ts b/src/apis/domains/home/api.ts new file mode 100644 index 00000000..7d9be603 --- /dev/null +++ b/src/apis/domains/home/api.ts @@ -0,0 +1,18 @@ +import { get } from "@apis/index"; +import { components } from "@typings/api/schema"; +import { ApiResponseType } from "@typings/commonType"; +import { AxiosResponse } from "axios"; + +type HomeResponse = components["schemas"]["HomeResponse"]; + +// 1. API 요청 함수 작성 및 타입 추가 +export const getAllScheduleList = async (): Promise => { + try { + const response: AxiosResponse> = await get("/main"); + + return response.data.data; + } catch (error) { + console.error("error", error); + return null; + } +}; diff --git a/src/apis/domains/home/queries.ts b/src/apis/domains/home/queries.ts new file mode 100644 index 00000000..2699d32c --- /dev/null +++ b/src/apis/domains/home/queries.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; +import { getAllScheduleList } from "./api"; + +const QUERY_KEY = { + LIST: "list", +}; + +// 2. 쿼리 작성 +export const useGetAllScheduleList = () => { + return useQuery({ + queryKey: [QUERY_KEY.LIST], + queryFn: () => getAllScheduleList(), // API 요청 함수 + staleTime: 1000 * 60 * 60, + gcTime: 1000 * 60 * 60 * 24, + }); +}; diff --git a/src/apis/domains/register/index.ts b/src/apis/domains/register/index.ts new file mode 100644 index 00000000..23754f2f --- /dev/null +++ b/src/apis/domains/register/index.ts @@ -0,0 +1,32 @@ +import { get } from "@apis/index"; +import { useQuery } from "@tanstack/react-query"; + +export const QUERY_KEY_POST = { + getPresignedUrl: "getPresignedUrl", +}; + +interface PresignedUrlPropTypes { + data: { fileName: string; url: string }; +} + +const fetchPresignedUrl = async () => { + try { + const response = await get("/api/image/upload"); + console.log(response.data); + + return response.data; + } catch (err) { + console.error("error", err); + } +}; + +export const usePresignedUrl = () => { + const { data } = useQuery({ + queryKey: [QUERY_KEY_POST.getPresignedUrl], + queryFn: () => fetchPresignedUrl(), + }); + + const fileName = data && data?.data?.fileName; + const url = data && data?.data?.url; + return { fileName, url }; +}; diff --git a/src/apis/index.ts b/src/apis/index.ts new file mode 100644 index 00000000..d9c85523 --- /dev/null +++ b/src/apis/index.ts @@ -0,0 +1,30 @@ +import axios from "axios"; + +export const instance = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL, + // withCredentials: true, + + headers: { + Authorization: `Bearer ${localStorage.getItem("accessToken") || ""}`, + }, +}); + +export function get(...args: Parameters) { + return instance.get(...args); +} + +export function post(...args: Parameters) { + return instance.post(...args); +} + +export function put(...args: Parameters) { + return instance.put(...args); +} + +export function patch(...args: Parameters) { + return instance.patch(...args); +} + +export function del(...args: Parameters) { + return instance.delete(...args); +} diff --git a/src/components/commons/chip/Chip.styled.ts b/src/components/commons/chip/Chip.styled.ts index a105a44d..35c3caa1 100644 --- a/src/components/commons/chip/Chip.styled.ts +++ b/src/components/commons/chip/Chip.styled.ts @@ -38,12 +38,12 @@ export const ChipWrapper = styled.div<{ color?: ChipsColorTypes }>` }} `; -export const ChipIcon = styled.span<{ iconColor?: string }>` +export const ChipIcon = styled.span<{ $iconColor?: string }>` width: 1.6rem; height: 1.6rem; - ${({ theme, iconColor }) => { - switch (iconColor) { + ${({ theme, $iconColor }) => { + switch ($iconColor) { case "pink": return ` color: ${theme.colors.pink_400}; diff --git a/src/components/commons/chip/Chip.tsx b/src/components/commons/chip/Chip.tsx index 098232e0..94a87280 100644 --- a/src/components/commons/chip/Chip.tsx +++ b/src/components/commons/chip/Chip.tsx @@ -14,7 +14,7 @@ interface ChipProps { const Chip = ({ label, color, icon, iconColor, onClick }: ChipProps) => { return ( - {icon && {icon}} + {icon && {icon}} {label} ); diff --git a/src/components/commons/hamburger/Hamburger.tsx b/src/components/commons/hamburger/Hamburger.tsx index 5ce1ce99..73b11087 100644 --- a/src/components/commons/hamburger/Hamburger.tsx +++ b/src/components/commons/hamburger/Hamburger.tsx @@ -23,6 +23,7 @@ const Hamburger = () => { const handlerOutside = (e: React.MouseEvent) => { closeHamburger(); + e.stopPropagation(); }; @@ -54,6 +55,7 @@ const Hamburger = () => { const handleKakaoLogin = (url: string) => { setNavigateUrl(url); requestKakaoLogin(); + closeHamburger(); }; return ( @@ -79,6 +81,7 @@ const Hamburger = () => { { navigate("/gig-register"); + closeHamburger(); }} > 내가 등록한 공연 @@ -87,6 +90,7 @@ const Hamburger = () => { { navigate("/lookup"); + closeHamburger(); }} > 내가 예매한 공연 @@ -103,6 +107,7 @@ const Hamburger = () => { { navigate("/nonmb-lookup"); + closeHamburger(); }} > 비회원 예매 조회 diff --git a/src/hooks/useTokenRefresher.tsx b/src/hooks/useTokenRefresher.tsx new file mode 100644 index 00000000..e413ca87 --- /dev/null +++ b/src/hooks/useTokenRefresher.tsx @@ -0,0 +1,77 @@ +import { instance } from "@apis/index"; +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; + +export default function TokenRefresher() { + const navigate = useNavigate(); + + useEffect(() => { + const interceptor = instance.interceptors.response.use( + // 성공적인 응답 처리 + (response) => { + console.log("Starting Request", response); + return response; + }, + async (error) => { + const originalConfig = error.config; // 기존에 수행하려고 했던 작업 + const msg = error.response.data.msg; // error msg from backend + const status = error.response.status; // 현재 발생한 에러 코드 + // access_token 재발급 + + if (status === 401) { + if (msg === "Expired Access Token. 토큰이 만료되었습니다") { + // console.log("토큰 재발급 요청"); + await instance + .post( + "/users/refresh-token", + {}, + { + // TODO: 쿠키로 변경 ? + headers: { + Authorization: `${localStorage.getItem("Authorization")}`, + Refresh: `${localStorage.getItem("Refresh")}`, + }, + } + ) + .then((res) => { + console.log("res: ", res); + // 새 토큰 저장 + localStorage.setItem("Authorization", res.headers.authorization); + localStorage.setItem("Refresh", res.headers.refresh); + + // 새로 응답받은 데이터로 토큰 만료로 실패한 요청에 대한 인증 시도 (header에 토큰 담아 보낼 때 사용) + originalConfig.headers["authorization"] = `Bearer ${res.headers.authorization}`; + originalConfig.headers["refresh"] = res.headers.refresh; + + // console.log("New access token obtained."); + // 새로운 토큰으로 재요청 + return instance(originalConfig); + }) + .catch(() => { + console.error("An error occurred while refreshing the token:", error); + }); + } + // refresh_token 재발급과 예외 처리 + // else if(msg == "만료된 리프레시 토큰입니다") { + else { + localStorage.clear(); + navigate("/main"); + alert("토큰이 만료되어 자동으로 로그아웃 되었습니다."); + } + } else if (status === 400 || status === 404 || status === 409) { + console.log("hi3"); + // window.alert(msg); + // console.log(msg) + } + // console.error('Error response:', error); + // 다른 모든 오류를 거부하고 처리 + return Promise.reject(error); + } + ); + return () => { + instance.interceptors.response.eject(interceptor); + }; + }, []); + + return <>; +} diff --git a/src/pages/main/Main.tsx b/src/pages/main/Main.tsx index a6900006..cc9cddb3 100644 --- a/src/pages/main/Main.tsx +++ b/src/pages/main/Main.tsx @@ -3,35 +3,44 @@ import * as S from "./Main.styled"; import Carousel from "./components/carousel/Carousel"; import Chips from "./components/chips/Chips"; +import Floating from "./components/floating/Floating"; + import Footer from "./components/footer/Footer"; import MainNavigation from "./components/mainNavigation/MainNavigation"; import Performance from "./components/performance/Performance"; -import Floating from "./components/floating/Floating"; -import { dummyData } from "./constants/dummyData"; -import { useAtom } from "jotai"; +import Loading from "@components/commons/loading/Loading"; + +import { useGetAllScheduleList } from "@apis/domains/home/queries"; import { navigateAtom } from "@stores/navigate"; +import { useAtom } from "jotai"; const Main = () => { + // 3. 훅 불러와서 사용 + const { data, isLoading } = useGetAllScheduleList(); + const [genre, setGenre] = useState("ALL"); + const [navigateUrl, setNavigateUrl] = useAtom(navigateAtom); const handleGenre = (value: string) => { setGenre(value); }; - const [navigateUrl, setNavigateUrl] = useAtom(navigateAtom); - - console.log("main", navigateUrl); - return ( - - - - - - -