From ee21782ef9db885b1d1148b8b155be4a84c8ddb1 Mon Sep 17 00:00:00 2001 From: pricelees Date: Sun, 27 Jul 2025 12:10:21 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=A0=ED=8A=B8?= =?UTF-8?q?=EC=97=94=EB=93=9C=20=EC=BD=94=EB=93=9C=EC=97=90=EC=84=9C=20API?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.env | 1 + frontend/.gitignore | 4 +- frontend/src/App.tsx | 40 +++++--- frontend/src/api/apiClient.ts | 81 +++++++++++++++ frontend/src/api/auth/authAPI.ts | 19 ++++ frontend/src/api/auth/authTypes.ts | 14 +++ frontend/src/api/member/memberAPI.ts | 10 ++ frontend/src/api/member/memberTypes.ts | 19 ++++ .../src/api/reservation/reservationAPI.ts | 70 +++++++++++++ .../src/api/reservation/reservationTypes.ts | 72 ++++++++++++++ frontend/src/api/theme/themeAPI.ts | 18 ++++ frontend/src/api/theme/themeTypes.ts | 23 +++++ frontend/src/api/time/timeAPI.ts | 18 ++++ frontend/src/api/time/timeTypes.ts | 27 +++++ frontend/src/components/AdminRoute.tsx | 28 ++++++ frontend/src/components/Navbar.tsx | 36 +++---- frontend/src/context/AuthContext.tsx | 73 ++++++++++++++ frontend/src/pages/HomePage.tsx | 10 +- frontend/src/pages/LoginPage.tsx | 34 ++++--- frontend/src/pages/MyReservationPage.tsx | 53 +++++++--- frontend/src/pages/ReservationPage.tsx | 55 +++++++---- frontend/src/pages/SignupPage.tsx | 14 ++- frontend/src/pages/admin/AdminNavbar.tsx | 40 +++----- frontend/src/pages/admin/ReservationPage.tsx | 98 ++++++++++++++----- frontend/src/pages/admin/ThemePage.tsx | 55 ++++++++--- frontend/src/pages/admin/TimePage.tsx | 69 ++++++++++--- frontend/src/pages/admin/WaitingPage.tsx | 59 +++++++---- frontend/vite.config.ts | 9 -- 28 files changed, 843 insertions(+), 206 deletions(-) create mode 100644 frontend/.env create mode 100644 frontend/src/api/apiClient.ts create mode 100644 frontend/src/api/auth/authAPI.ts create mode 100644 frontend/src/api/auth/authTypes.ts create mode 100644 frontend/src/api/member/memberAPI.ts create mode 100644 frontend/src/api/member/memberTypes.ts create mode 100644 frontend/src/api/reservation/reservationAPI.ts create mode 100644 frontend/src/api/reservation/reservationTypes.ts create mode 100644 frontend/src/api/theme/themeAPI.ts create mode 100644 frontend/src/api/theme/themeTypes.ts create mode 100644 frontend/src/api/time/timeAPI.ts create mode 100644 frontend/src/api/time/timeTypes.ts create mode 100644 frontend/src/components/AdminRoute.tsx create mode 100644 frontend/src/context/AuthContext.tsx diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 00000000..2717bd56 --- /dev/null +++ b/frontend/.env @@ -0,0 +1 @@ +VITE_API_BASE_URL = "http://localhost:8080" \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index d44bd12c..f804f4f7 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -23,4 +23,6 @@ dist-ssr *.sln *.sw? -.DS_Store \ No newline at end of file +.DS_Store + +.env \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cc7e4b98..225f6538 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,6 +11,8 @@ import AdminReservationPage from './pages/admin/ReservationPage'; import AdminTimePage from './pages/admin/TimePage'; import AdminThemePage from './pages/admin/ThemePage'; import AdminWaitingPage from './pages/admin/WaitingPage'; +import { AuthProvider } from './context/AuthContext'; +import AdminRoute from './components/AdminRoute'; const AdminRoutes = () => ( @@ -26,22 +28,28 @@ const AdminRoutes = () => ( function App() { return ( - - - } /> - - - } /> - } /> - } /> - } /> - } /> - - - } /> - - + + + + + + + } /> + + + } /> + } /> + } /> + } /> + } /> + + + } /> + + + ); } diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts new file mode 100644 index 00000000..83090c44 --- /dev/null +++ b/frontend/src/api/apiClient.ts @@ -0,0 +1,81 @@ +import axios, { type AxiosError, type AxiosRequestConfig, type Method } from 'axios'; + +const apiClient = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080', + timeout: 10000, +}); + +export const isLoginRequiredError = (error: any): boolean => { + if (!axios.isAxiosError(error) || !error.response) { + return false; + } + const LOGIN_REQUIRED_ERROR_CODE = ['A001', 'A002', 'A003', 'A006']; + const code = error.response?.data?.code; + return code && LOGIN_REQUIRED_ERROR_CODE.includes(code); +} + +async function request( + method: Method, + endpoint: string, + data: object = {}, + isRequiredAuth: boolean = false +): Promise { + const config: AxiosRequestConfig = { + method, + url: endpoint, + headers: { + 'Content-Type': 'application/json' + }, + }; + + if (isRequiredAuth) { + const accessToken = localStorage.getItem('accessToken'); + if (accessToken) { + if (!config.headers) { + config.headers = {}; + } + config.headers['Authorization'] = `Bearer ${accessToken}`; + } + } + + if (method.toUpperCase() !== 'GET') { + config.data = data; + } + + try { + const response = await apiClient.request(config); + return response.data.data; + } catch (error: unknown) { + const axiosError = error as AxiosError<{ code: string, message: string }>; + console.error('API 요청 실패:', axiosError); + throw axiosError; + } +} + +async function get(endpoint: string, isRequiredAuth: boolean = false): Promise { + return request('GET', endpoint, {}, isRequiredAuth); +} + +async function post(endpoint: string, data: object = {}, isRequiredAuth: boolean = false): Promise { + return request('POST', endpoint, data, isRequiredAuth); +} + +async function put(endpoint: string, data: object = {}, isRequiredAuth: boolean = false): Promise { + return request('PUT', endpoint, data, isRequiredAuth); +} + +async function patch(endpoint: string, data: object = {}, isRequiredAuth: boolean = false): Promise { + return request('PATCH', endpoint, data, isRequiredAuth); +} + +async function del(endpoint: string, isRequiredAuth: boolean = false): Promise { + return request('DELETE', endpoint, {}, isRequiredAuth); +} + +export default { + get, + post, + put, + patch, + del +}; diff --git a/frontend/src/api/auth/authAPI.ts b/frontend/src/api/auth/authAPI.ts new file mode 100644 index 00000000..a9f34dfd --- /dev/null +++ b/frontend/src/api/auth/authAPI.ts @@ -0,0 +1,19 @@ +import apiClient from '@_api/apiClient'; +import type { LoginRequest, LoginResponse, LoginCheckResponse } from './authTypes'; + + +export const login = async (data: LoginRequest): Promise => { + const response = await apiClient.post('/login', data, false); + localStorage.setItem('accessToken', response.accessToken); + + return response; +}; + +export const checkLogin = async (): Promise => { + return await apiClient.get('/login/check', true); +}; + +export const logout = async (): Promise => { + await apiClient.post('/logout', {}, true); + localStorage.removeItem('accessToken'); +}; diff --git a/frontend/src/api/auth/authTypes.ts b/frontend/src/api/auth/authTypes.ts new file mode 100644 index 00000000..6889425f --- /dev/null +++ b/frontend/src/api/auth/authTypes.ts @@ -0,0 +1,14 @@ +export interface LoginRequest { + email: string; + password: string; +} + +export interface LoginResponse { + accessToken: string; +} + +export interface LoginCheckResponse { + name: string; + role: 'ADMIN' | 'MEMBER'; +} + diff --git a/frontend/src/api/member/memberAPI.ts b/frontend/src/api/member/memberAPI.ts new file mode 100644 index 00000000..219e7f3a --- /dev/null +++ b/frontend/src/api/member/memberAPI.ts @@ -0,0 +1,10 @@ +import apiClient from "@_api/apiClient"; +import type { MemberRetrieveListResponse, SignupRequest, SignupResponse } from "./memberTypes"; + +export const fetchMembers = async (): Promise => { + return await apiClient.get('/members', true); +}; + +export const signup = async (data: SignupRequest): Promise => { + return await apiClient.post('/members', data, false); +}; diff --git a/frontend/src/api/member/memberTypes.ts b/frontend/src/api/member/memberTypes.ts new file mode 100644 index 00000000..45d90e86 --- /dev/null +++ b/frontend/src/api/member/memberTypes.ts @@ -0,0 +1,19 @@ +export interface MemberRetrieveResponse { + id: number; + name: string; +} + +export interface MemberRetrieveListResponse { + members: MemberRetrieveResponse[]; +} + +export interface SignupRequest { + email: string; + password: string; + name: string; +} + +export interface SignupResponse { + id: number; + name: string; +} diff --git a/frontend/src/api/reservation/reservationAPI.ts b/frontend/src/api/reservation/reservationAPI.ts new file mode 100644 index 00000000..276ce06f --- /dev/null +++ b/frontend/src/api/reservation/reservationAPI.ts @@ -0,0 +1,70 @@ +import apiClient from "@_api/apiClient"; +import type { + AdminReservationCreateRequest, + MyReservationRetrieveListResponse, + ReservationCreateWithPaymentRequest, + ReservationRetrieveListResponse, + ReservationRetrieveResponse, + ReservationSearchQuery, + WaitingCreateRequest +} from "./reservationTypes"; + +// GET /reservations +export const fetchReservations = async (): Promise => { + return await apiClient.get('/reservations', true); +}; + +// GET /reservations-mine +export const fetchMyReservations = async (): Promise => { + return await apiClient.get('/reservations-mine', true); +}; + +// GET /reservations/search +export const searchReservations = async (params: ReservationSearchQuery): Promise => { + const query = new URLSearchParams(); + if (params.themeId) query.append('themeId', params.themeId.toString()); + if (params.memberId) query.append('memberId', params.memberId.toString()); + if (params.dateFrom) query.append('dateFrom', params.dateFrom); + if (params.dateTo) query.append('dateTo', params.dateTo); + return await apiClient.get(`/reservations/search?${query.toString()}`, true); +}; + +// DELETE /reservations/{id} +export const cancelReservationByAdmin = async (id: number): Promise => { + return await apiClient.del(`/reservations/${id}`, true); +}; + +// POST /reservations +export const createReservationWithPayment = async (data: ReservationCreateWithPaymentRequest): Promise => { + return await apiClient.post('/reservations', data, true); +}; + +// POST /reservations/admin +export const createReservationByAdmin = async (data: AdminReservationCreateRequest): Promise => { + return await apiClient.post('/reservations/admin', data, true); +}; + +// GET /reservations/waiting +export const fetchWaitingReservations = async (): Promise => { + return await apiClient.get('/reservations/waiting', true); +}; + +// POST /reservations/waiting +export const createWaiting = async (data: WaitingCreateRequest): Promise => { + return await apiClient.post('/reservations/waiting', data, true); +}; + +// DELETE /reservations/waiting/{id} +export const cancelWaiting = async (id: number): Promise => { + return await apiClient.del(`/reservations/waiting/${id}`, true); +}; + +// POST /reservations/waiting/{id}/confirm +export const confirmWaiting = async (id: number): Promise => { + return await apiClient.post(`/reservations/waiting/${id}/confirm`, {}, true); +}; + +// POST /reservations/waiting/{id}/reject +export const rejectWaiting = async (id: number): Promise => { + return await apiClient.post(`/reservations/waiting/${id}/reject`, {}, true); +}; diff --git a/frontend/src/api/reservation/reservationTypes.ts b/frontend/src/api/reservation/reservationTypes.ts new file mode 100644 index 00000000..5bffd87f --- /dev/null +++ b/frontend/src/api/reservation/reservationTypes.ts @@ -0,0 +1,72 @@ +import type { MemberRetrieveResponse } from '@_api/member/memberTypes'; +import type { ThemeRetrieveResponse } from '@_api/theme/themeTypes'; +import type { TimeRetrieveResponse } from '@_api/time/timeTypes'; + +export const ReservationStatus = { + CONFIRMED: 'CONFIRMED', + CONFIRMED_PAYMENT_REQUIRED: 'CONFIRMED_PAYMENT_REQUIRED', + WAITING: 'WAITING', +} as const; + +export type ReservationStatus = + | typeof ReservationStatus.CONFIRMED + | typeof ReservationStatus.CONFIRMED_PAYMENT_REQUIRED + | typeof ReservationStatus.WAITING; + +export interface MyReservationRetrieveResponse { + id: number; + themeName: string; + date: string; + time: string; + status: ReservationStatus; + rank: number; + paymentKey: string | null; + amount: number | null; +} + +export interface MyReservationRetrieveListResponse { + reservations: MyReservationRetrieveResponse[]; +} + +export interface ReservationRetrieveResponse { + id: number; + date: string; + member: MemberRetrieveResponse; + time: TimeRetrieveResponse; + theme: ThemeRetrieveResponse; + status: ReservationStatus; +} + +export interface ReservationRetrieveListResponse { + reservations: ReservationRetrieveResponse[]; +} + +export interface AdminReservationCreateRequest { + date: string; + timeId: number; + themeId: number; + memberId: number; +} + +export interface ReservationCreateWithPaymentRequest { + date: string; + timeId: number; + themeId: number; + paymentKey: string; + orderId: string; + amount: number; + paymentType: string; +} + +export interface WaitingCreateRequest { + date: string; + timeId: number; + themeId: number; +} + +export interface ReservationSearchQuery { + themeId?: number; + memberId?: number; + dateFrom?: string; + dateTo?: string; +} diff --git a/frontend/src/api/theme/themeAPI.ts b/frontend/src/api/theme/themeAPI.ts new file mode 100644 index 00000000..c653a9e8 --- /dev/null +++ b/frontend/src/api/theme/themeAPI.ts @@ -0,0 +1,18 @@ +import apiClient from "@_api/apiClient"; +import type { ThemeCreateRequest, ThemeCreateResponse, ThemeRetrieveListResponse } from "./themeTypes"; + +export const createTheme = async (data: ThemeCreateRequest): Promise => { + return await apiClient.post('/themes', data, true); +}; + +export const fetchThemes = async (): Promise => { + return await apiClient.get('/themes', true); +}; + +export const mostReservedThemes = async (count: number = 10): Promise => { + return await apiClient.get(`/themes/most-reserved-last-week?count=${count}`, false); +}; + +export const delTheme = async (id: number): Promise => { + return await apiClient.del(`/themes/${id}`, true); +}; diff --git a/frontend/src/api/theme/themeTypes.ts b/frontend/src/api/theme/themeTypes.ts new file mode 100644 index 00000000..d1721953 --- /dev/null +++ b/frontend/src/api/theme/themeTypes.ts @@ -0,0 +1,23 @@ +export interface ThemeCreateRequest { + name: string; + description: string; + thumbnail: string; +} + +export interface ThemeCreateResponse { + id: number; + name: string; + description: string; + thumbnail: string; +} + +export interface ThemeRetrieveResponse { + id: number; + name: string; + description: string; + thumbnail: string; +} + +export interface ThemeRetrieveListResponse { + themes: ThemeRetrieveResponse[]; +} diff --git a/frontend/src/api/time/timeAPI.ts b/frontend/src/api/time/timeAPI.ts new file mode 100644 index 00000000..2a2d6ac2 --- /dev/null +++ b/frontend/src/api/time/timeAPI.ts @@ -0,0 +1,18 @@ +import apiClient from "@_api/apiClient"; +import type { TimeCreateRequest, TimeCreateResponse, TimeRetrieveListResponse, TimeWithAvailabilityListResponse } from "./timeTypes"; + +export const createTime = async (data: TimeCreateRequest): Promise => { + return await apiClient.post('/times', data, true); +} + +export const fetchTimes = async (): Promise => { + return await apiClient.get('/times', true); +}; + +export const delTime = async (id: number): Promise => { + return await apiClient.del(`/times/${id}`, true); +}; + +export const fetchTimesWithAvailability = async (date: string, themeId: number): Promise => { + return await apiClient.get(`/times/search?date=${date}&themeId=${themeId}`, true); +}; diff --git a/frontend/src/api/time/timeTypes.ts b/frontend/src/api/time/timeTypes.ts new file mode 100644 index 00000000..408e8f7f --- /dev/null +++ b/frontend/src/api/time/timeTypes.ts @@ -0,0 +1,27 @@ +export interface TimeCreateRequest { + startAt: string; +} + +export interface TimeCreateResponse { + id: number; + startAt: string; +} + +export interface TimeRetrieveResponse { + id: number; + startAt: string; +} + +export interface TimeRetrieveListResponse { + times: TimeCreateResponse[]; +} + +export interface TimeWithAvailabilityResponse { + id: number; + startAt: string; + isAvailable: boolean; +} + +export interface TimeWithAvailabilityListResponse { + times: TimeWithAvailabilityResponse[]; +} \ No newline at end of file diff --git a/frontend/src/components/AdminRoute.tsx b/frontend/src/components/AdminRoute.tsx new file mode 100644 index 00000000..e8354014 --- /dev/null +++ b/frontend/src/components/AdminRoute.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; + +const AdminRoute: React.FC<{ children: JSX.Element }> = ({ children }) => { + const { loggedIn, role, loading } = useAuth(); + const location = useLocation(); + + if (loading) { + return
Loading...
; // Or a proper spinner component + } + + if (!loggedIn) { + // Not logged in, redirect to login page. No alert needed here + // as the user is simply redirected. + return ; + } + + if (role !== 'ADMIN') { + // Logged in but not an admin, show alert and redirect. + alert('접근 권한이 없어요. 관리자에게 문의해주세요.'); + return ; + } + + return children; +}; + +export default AdminRoute; diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 7b12c8db..83c75a2c 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,33 +1,21 @@ +import { checkLogin } from '@_api/auth/authAPI'; import React, { useEffect, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; -import axios from 'axios'; +import { useAuth } from 'src/context/AuthContext'; const Navbar: React.FC = () => { - const [loggedIn, setLoggedIn] = useState(false); - const [userName, setUserName] = useState('Profile'); + const { loggedIn, userName, logout } = useAuth(); const navigate = useNavigate(); - - useEffect(() => { - axios.get('/api/login/check') - .then(response => { - setUserName(response.data.data.name); - setLoggedIn(true); - }) - .catch(() => { - setLoggedIn(false); - }); - }, [navigate]); - - const handleLogout = (e: React.MouseEvent) => { + + const handleLogout = async (e: React.MouseEvent) => { e.preventDefault(); - axios.post('/api/logout') - .then(() => { - setLoggedIn(false); - setUserName('Profile'); - window.location.href = "/"; - }) - .catch(error => console.error('Logout failed:', error)); - }; + try { + await logout(); + navigate('/'); + } catch (error) { + console.error('Logout failed:', error); + } + } return (