[#22] 프론트엔드 React 전환 및 인증 API 수정 #23

Merged
pricelees merged 9 commits from refactor/#22 into main 2025-07-27 03:39:20 +00:00
28 changed files with 843 additions and 206 deletions
Showing only changes of commit ee21782ef9 - Show all commits

1
frontend/.env Normal file
View File

@ -0,0 +1 @@
VITE_API_BASE_URL = "http://localhost:8080"

4
frontend/.gitignore vendored
View File

@ -23,4 +23,6 @@ dist-ssr
*.sln
*.sw?
.DS_Store
.DS_Store
.env

View File

@ -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 = () => (
<AdminLayout>
@ -26,22 +28,28 @@ const AdminRoutes = () => (
function App() {
return (
<Router>
<Routes>
<Route path="/admin/*" element={<AdminRoutes />} />
<Route path="/*" element={
<Layout>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignupPage />} />
<Route path="/reservation" element={<ReservationPage />} />
<Route path="/reservation-mine" element={<MyReservationPage />} />
</Routes>
</Layout>
} />
</Routes>
</Router>
<AuthProvider>
<Router>
<Routes>
<Route path="/admin/*" element={
<AdminRoute>
<AdminRoutes />
</AdminRoute>
} />
<Route path="/*" element={
<Layout>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignupPage />} />
<Route path="/reservation" element={<ReservationPage />} />
<Route path="/reservation-mine" element={<MyReservationPage />} />
</Routes>
</Layout>
} />
</Routes>
</Router>
</AuthProvider>
);
}

View File

@ -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<T>(
method: Method,
endpoint: string,
data: object = {},
isRequiredAuth: boolean = false
): Promise<T> {
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<T>(endpoint: string, isRequiredAuth: boolean = false): Promise<T> {
return request<T>('GET', endpoint, {}, isRequiredAuth);
}
async function post<T>(endpoint: string, data: object = {}, isRequiredAuth: boolean = false): Promise<T> {
return request<T>('POST', endpoint, data, isRequiredAuth);
}
async function put<T>(endpoint: string, data: object = {}, isRequiredAuth: boolean = false): Promise<T> {
return request<T>('PUT', endpoint, data, isRequiredAuth);
}
async function patch<T>(endpoint: string, data: object = {}, isRequiredAuth: boolean = false): Promise<T> {
return request<T>('PATCH', endpoint, data, isRequiredAuth);
}
async function del<T>(endpoint: string, isRequiredAuth: boolean = false): Promise<T> {
return request<T>('DELETE', endpoint, {}, isRequiredAuth);
}
export default {
get,
post,
put,
patch,
del
};

View File

@ -0,0 +1,19 @@
import apiClient from '@_api/apiClient';
import type { LoginRequest, LoginResponse, LoginCheckResponse } from './authTypes';
export const login = async (data: LoginRequest): Promise<LoginResponse> => {
const response = await apiClient.post<LoginResponse>('/login', data, false);
localStorage.setItem('accessToken', response.accessToken);
return response;
};
export const checkLogin = async (): Promise<LoginCheckResponse> => {
return await apiClient.get<LoginCheckResponse>('/login/check', true);
};
export const logout = async (): Promise<void> => {
await apiClient.post('/logout', {}, true);
localStorage.removeItem('accessToken');
};

View File

@ -0,0 +1,14 @@
export interface LoginRequest {
email: string;
password: string;
}
export interface LoginResponse {
accessToken: string;
}
export interface LoginCheckResponse {
name: string;
role: 'ADMIN' | 'MEMBER';
}

View File

@ -0,0 +1,10 @@
import apiClient from "@_api/apiClient";
import type { MemberRetrieveListResponse, SignupRequest, SignupResponse } from "./memberTypes";
export const fetchMembers = async (): Promise<MemberRetrieveListResponse> => {
return await apiClient.get<MemberRetrieveListResponse>('/members', true);
};
export const signup = async (data: SignupRequest): Promise<SignupResponse> => {
return await apiClient.post('/members', data, false);
};

View File

@ -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;
}

View File

@ -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<ReservationRetrieveListResponse> => {
return await apiClient.get<ReservationRetrieveListResponse>('/reservations', true);
};
// GET /reservations-mine
export const fetchMyReservations = async (): Promise<MyReservationRetrieveListResponse> => {
return await apiClient.get<MyReservationRetrieveListResponse>('/reservations-mine', true);
};
// GET /reservations/search
export const searchReservations = async (params: ReservationSearchQuery): Promise<ReservationRetrieveListResponse> => {
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<ReservationRetrieveListResponse>(`/reservations/search?${query.toString()}`, true);
};
// DELETE /reservations/{id}
export const cancelReservationByAdmin = async (id: number): Promise<void> => {
return await apiClient.del(`/reservations/${id}`, true);
};
// POST /reservations
export const createReservationWithPayment = async (data: ReservationCreateWithPaymentRequest): Promise<ReservationRetrieveResponse> => {
return await apiClient.post<ReservationRetrieveResponse>('/reservations', data, true);
};
// POST /reservations/admin
export const createReservationByAdmin = async (data: AdminReservationCreateRequest): Promise<ReservationRetrieveResponse> => {
return await apiClient.post<ReservationRetrieveResponse>('/reservations/admin', data, true);
};
// GET /reservations/waiting
export const fetchWaitingReservations = async (): Promise<ReservationRetrieveListResponse> => {
return await apiClient.get<ReservationRetrieveListResponse>('/reservations/waiting', true);
};
// POST /reservations/waiting
export const createWaiting = async (data: WaitingCreateRequest): Promise<ReservationRetrieveResponse> => {
return await apiClient.post<ReservationRetrieveResponse>('/reservations/waiting', data, true);
};
// DELETE /reservations/waiting/{id}
export const cancelWaiting = async (id: number): Promise<void> => {
return await apiClient.del(`/reservations/waiting/${id}`, true);
};
// POST /reservations/waiting/{id}/confirm
export const confirmWaiting = async (id: number): Promise<void> => {
return await apiClient.post(`/reservations/waiting/${id}/confirm`, {}, true);
};
// POST /reservations/waiting/{id}/reject
export const rejectWaiting = async (id: number): Promise<void> => {
return await apiClient.post(`/reservations/waiting/${id}/reject`, {}, true);
};

View File

@ -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;
}

View File

@ -0,0 +1,18 @@
import apiClient from "@_api/apiClient";
import type { ThemeCreateRequest, ThemeCreateResponse, ThemeRetrieveListResponse } from "./themeTypes";
export const createTheme = async (data: ThemeCreateRequest): Promise<ThemeCreateResponse> => {
return await apiClient.post<ThemeCreateResponse>('/themes', data, true);
};
export const fetchThemes = async (): Promise<ThemeRetrieveListResponse> => {
return await apiClient.get<ThemeRetrieveListResponse>('/themes', true);
};
export const mostReservedThemes = async (count: number = 10): Promise<ThemeRetrieveListResponse> => {
return await apiClient.get<ThemeRetrieveListResponse>(`/themes/most-reserved-last-week?count=${count}`, false);
};
export const delTheme = async (id: number): Promise<void> => {
return await apiClient.del(`/themes/${id}`, true);
};

View File

@ -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[];
}

View File

@ -0,0 +1,18 @@
import apiClient from "@_api/apiClient";
import type { TimeCreateRequest, TimeCreateResponse, TimeRetrieveListResponse, TimeWithAvailabilityListResponse } from "./timeTypes";
export const createTime = async (data: TimeCreateRequest): Promise<TimeCreateResponse> => {
return await apiClient.post<TimeCreateResponse>('/times', data, true);
}
export const fetchTimes = async (): Promise<TimeRetrieveListResponse> => {
return await apiClient.get<TimeRetrieveListResponse>('/times', true);
};
export const delTime = async (id: number): Promise<void> => {
return await apiClient.del(`/times/${id}`, true);
};
export const fetchTimesWithAvailability = async (date: string, themeId: number): Promise<TimeWithAvailabilityListResponse> => {
return await apiClient.get<TimeWithAvailabilityListResponse>(`/times/search?date=${date}&themeId=${themeId}`, true);
};

View File

@ -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[];
}

View File

@ -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 <div>Loading...</div>; // 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 <Navigate to="/login" state={{ from: location }} replace />;
}
if (role !== 'ADMIN') {
// Logged in but not an admin, show alert and redirect.
alert('접근 권한이 없어요. 관리자에게 문의해주세요.');
return <Navigate to="/" replace />;
}
return children;
};
export default AdminRoute;

View File

@ -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 (
<nav className="navbar navbar-expand-lg navbar-light bg-light">

View File

@ -0,0 +1,73 @@
import React, { createContext, useState, useEffect, ReactNode, useContext } from 'react';
import { checkLogin as apiCheckLogin, login as apiLogin, logout as apiLogout } from '@_api/auth/authAPI';
import type { LoginRequest, LoginCheckResponse } from '@_api/auth/authTypes';
interface AuthContextType {
loggedIn: boolean;
userName: string | null;
role: 'ADMIN' | 'MEMBER' | null;
loading: boolean; // Add loading state to type
login: (data: LoginRequest) => Promise<LoginCheckResponse>;
logout: () => Promise<void>;
checkLogin: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [loggedIn, setLoggedIn] = useState(false);
const [userName, setUserName] = useState<string | null>(null);
const [role, setRole] = useState<'ADMIN' | 'MEMBER' | null>(null);
const [loading, setLoading] = useState(true); // Add loading state
const checkLogin = async () => {
try {
const response = await apiCheckLogin();
setLoggedIn(true);
setUserName(response.name);
setRole(response.role);
} catch (error) {
setLoggedIn(false);
setUserName(null);
setRole(null);
localStorage.removeItem('accessToken');
} finally {
setLoading(false); // Set loading to false after check is complete
}
};
useEffect(() => {
checkLogin();
}, []);
const login = async (data: LoginRequest) => {
const response = await apiLogin(data);
await checkLogin();
return response;
};
const logout = async () => {
try {
await apiLogout();
} finally {
setLoggedIn(false);
setUserName(null);
setRole(null);
localStorage.removeItem('accessToken');
}
};
return (
<AuthContext.Provider value={{ loggedIn, userName, role, loading, login, logout, checkLogin }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@ -1,13 +1,15 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { mostReservedThemes } from '@_api/theme/themeAPI';
const HomePage: React.FC = () => {
const [ranking, setRanking] = useState<any[]>([]);
useEffect(() => {
axios.get('/api/themes/most-reserved-last-week?count=10')
.then(res => setRanking(res.data.data.themes))
.catch(err => console.error('Error fetching ranking:', err));
const fetchData = async () => {
await mostReservedThemes(10).then(response => setRanking(response.themes))
};
fetchData().catch(err => console.error('Error fetching ranking:', err));
}, []);
return (

View File

@ -1,20 +1,32 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { useLocation, useNavigate } from 'react-router-dom';
import type { LoginRequest } from '@_api/auth/authTypes';
import { useAuth } from '../context/AuthContext';
const LoginPage: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { login } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const handleLogin = () => {
axios.post('/api/login', { email, password })
.then(() => navigate('/'))
.catch(error => {
alert('Login failed');
console.error(error);
});
};
const from = location.state?.from?.pathname || '/';
const handleLogin = async () => {
try {
const request: LoginRequest = { email, password };
await login(request);
alert('로그인에 성공했어요!');
navigate(from, { replace: true });
} catch (error: any) {
const message = error.response?.data?.message || '로그인에 실패했어요. 이메일과 비밀번호를 확인해주세요.';
alert(message);
console.error('로그인 실패:', error);
setEmail('');
setPassword('');
}
}
return (
<div className="content-container" style={{ width: '300px' }}>
@ -33,4 +45,4 @@ const LoginPage: React.FC = () => {
);
};
export default LoginPage;
export default LoginPage;

View File

@ -1,24 +1,51 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
import { cancelWaiting, fetchMyReservations } from '@_api/reservation/reservationAPI';
import type { MyReservationRetrieveResponse } from '@_api/reservation/reservationTypes';
import { ReservationStatus } from '@_api/reservation/reservationTypes';
import { isLoginRequiredError } from '@_api/apiClient';
const MyReservationPage: React.FC = () => {
const [reservations, setReservations] = useState<any[]>([]);
const [reservations, setReservations] = useState<MyReservationRetrieveResponse[]>([]);
const navigate = useNavigate();
const handleError = (err: any) => {
if (isLoginRequiredError(err)) {
alert('로그인이 필요해요.');
navigate('/login', { state: { from: location } });
} else {
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
alert(message);
console.error(err);
}
};
useEffect(() => {
axios.get('/api/reservations-mine')
.then(res => setReservations(res.data.data.reservations))
.catch(err => console.error(err));
fetchMyReservations()
.then(res => setReservations(res.reservations))
.catch(handleError);
}, []);
const cancelWaiting = (id: number) => {
axios.delete(`/api/reservations/waiting/${id}`)
const _cancelWaiting = (id: number) => {
cancelWaiting(id)
.then(() => {
alert('예약 대기가 취소되었습니다.');
setReservations(reservations.filter(r => r.id !== id));
})
.catch(err => {
alert('취소에 실패했습니다: ' + err.response.data.message);
});
.catch(handleError);
};
const getStatusText = (status: ReservationStatus, rank: number) => {
if (status === ReservationStatus.CONFIRMED) {
return '예약';
}
if (status === ReservationStatus.CONFIRMED_PAYMENT_REQUIRED) {
return '예약 - 결제 필요';
}
if (status === ReservationStatus.WAITING) {
return `${rank}번째 예약 대기`;
}
return '';
};
return (
@ -44,10 +71,10 @@ const MyReservationPage: React.FC = () => {
<td>{r.themeName}</td>
<td>{r.date}</td>
<td>{r.time}</td>
<td>{r.status.includes('CONFIRMED') ? (r.status === 'CONFIRMED' ? '예약' : '예약 - 결제 필요') : r.rank + '번째 예약 대기'}</td>
<td>{getStatusText(r.status, r.rank)}</td>
<td>
{r.status === 'WAITING' &&
<button className="btn btn-danger" onClick={() => cancelWaiting(r.id)}></button>}
{r.status === ReservationStatus.WAITING &&
<button className="btn btn-danger" onClick={() => _cancelWaiting(r.id)}></button>}
</td>
<td>{r.paymentKey}</td>
<td>{r.amount}</td>

View File

@ -1,7 +1,13 @@
import React, { useEffect, useState, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import Flatpickr from 'react-flatpickr';
import 'flatpickr/dist/flatpickr.min.css';
import axios from 'axios';
import { fetchThemes } from '@_api/theme/themeAPI';
import { fetchTimesWithAvailability } from '@_api/time/timeAPI';
import { createReservationWithPayment, createWaiting } from '@_api/reservation/reservationAPI';
import type { ThemeRetrieveResponse } from '@_api/theme/themeTypes';
import type { TimeWithAvailabilityResponse } from '@_api/time/timeTypes';
import { isLoginRequiredError } from '@_api/apiClient';
declare global {
interface Window {
@ -11,12 +17,25 @@ declare global {
const ReservationPage: React.FC = () => {
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
const [themes, setThemes] = useState<any[]>([]);
const [themes, setThemes] = useState<ThemeRetrieveResponse[]>([]);
const [selectedTheme, setSelectedTheme] = useState<number | null>(null);
const [times, setTimes] = useState<any[]>([]);
const [times, setTimes] = useState<TimeWithAvailabilityResponse[]>([]);
const [selectedTime, setSelectedTime] = useState<{ id: number, isAvailable: boolean } | null>(null);
const paymentWidgetRef = useRef<any>(null);
const paymentMethodsRef = useRef<any>(null);
const navigate = useNavigate();
const location = useLocation();
const handleError = (err: any) => {
if (isLoginRequiredError(err)) {
alert('로그인이 필요해요.');
navigate('/login', { state: { from: location } });
} else {
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
alert(message);
console.error(err);
}
};
useEffect(() => {
const script = document.createElement('script');
@ -37,17 +56,18 @@ const ReservationPage: React.FC = () => {
paymentMethodsRef.current = paymentMethods;
};
axios.get('/api/themes').then(res => setThemes(res.data.data.themes));
fetchThemes().then(res => setThemes(res.themes)).catch(handleError);
}, []);
useEffect(() => {
if (selectedDate && selectedTheme) {
const dateStr = selectedDate.toLocaleDateString('en-CA')
axios.get(`/api/times/search?date=${dateStr}&themeId=${selectedTheme}`)
const dateStr = selectedDate.toLocaleDateString('en-CA');
fetchTimesWithAvailability(dateStr, selectedTheme)
.then(res => {
setTimes(res.data.data.times)
setTimes(res.times);
setSelectedTime(null);
});
})
.catch(handleError);
}
}, [selectedDate, selectedTheme]);
@ -78,17 +98,17 @@ const ReservationPage: React.FC = () => {
orderId: data.orderId,
amount: data.amount,
paymentType: data.paymentType,
}
axios.post("/api/reservations", reservationPaymentRequest)
};
createReservationWithPayment(reservationPaymentRequest)
.then(() => {
alert("예약이 완료되었습니다.");
window.location.href = "/";
})
.catch((err: any) => {
alert("예약에 실패했습니다: " + err.response.data.message);
});
.catch(handleError);
}).catch(function (error: any) {
alert(error.code + " :" + error.message);
// This is a client-side error from Toss Payments, not our API
console.error("Payment request error:", error);
alert("결제 요청 중 오류가 발생했습니다.");
});
};
@ -104,15 +124,12 @@ const ReservationPage: React.FC = () => {
timeId: selectedTime.id,
};
axios.post('/api/reservations/waiting', reservationData)
createWaiting(reservationData)
.then(() => {
alert('예약 대기가 완료되었습니다.');
window.location.href = "/";
})
.catch(error => {
alert("예약 대기에 실패했습니다.");
console.error(error);
});
.catch(handleError);
}
const isReserveButtonDisabled = !selectedDate || !selectedTheme || !selectedTime || !selectedTime.isAvailable;

View File

@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { signup } from '@_api/member/memberAPI';
import type { SignupRequest } from '@_api/member/memberTypes';
const SignupPage: React.FC = () => {
const [email, setEmail] = useState('');
@ -8,11 +9,14 @@ const SignupPage: React.FC = () => {
const [name, setName] = useState('');
const navigate = useNavigate();
const handleSignup = () => {
axios.post('/api/members', { email, password, name })
.then(() => navigate('/login'))
const handleSignup = async () => {
const request: SignupRequest = { email, password, name };
await signup(request)
.then((response) => {
alert(`${response.name}님, 회원가입을 축하드려요. 로그인 후 이용해주세요!`);
navigate('/login')
})
.catch(error => {
alert('Signup failed');
console.error(error);
});
};

View File

@ -1,32 +1,20 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import { Link, useNavigate } from 'react-router-dom';
import axios from 'axios';
import { useAuth } from '../../context/AuthContext';
const AdminNavbar: 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);
// Handle logout error if needed
}
};
return (
@ -59,11 +47,9 @@ const AdminNavbar: React.FC = () => {
<li className="nav-item dropdown">
<a className="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<img className="profile-image" src="/image/default-profile.png" alt="Profile" />
<span id="profile-name">{userName}</span>
<span id="profile-name">{userName || 'Profile'}</span>
</a>
<ul className="dropdown-menu" aria-labelledby="navbarDropdown">
<li><Link className="dropdown-item" to="/reservation-mine">My Reservation</Link></li>
<li><hr className="dropdown-divider" /></li>
<li><a className="dropdown-item" href="#" onClick={handleLogout}>Logout</a></li>
</ul>
</li>
@ -74,4 +60,4 @@ const AdminNavbar: React.FC = () => {
);
};
export default AdminNavbar;
export default AdminNavbar;

View File

@ -1,26 +1,53 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { useLocation, useNavigate } from 'react-router-dom';
import {
cancelReservationByAdmin,
createReservationByAdmin,
fetchReservations,
searchReservations
} from '@_api/reservation/reservationAPI';
import { fetchMembers } from '@_api/member/memberAPI';
import { fetchThemes } from '@_api/theme/themeAPI';
import { fetchTimes } from '@_api/time/timeAPI';
import type { ReservationRetrieveResponse } from '@_api/reservation/reservationTypes';
import type { MemberRetrieveResponse } from '@_api/member/memberTypes';
import type { ThemeRetrieveResponse } from '@_api/theme/themeTypes';
import type { TimeRetrieveResponse } from '@_api/time/timeTypes';
import { isLoginRequiredError } from '@_api/apiClient';
const AdminReservationPage: React.FC = () => {
const [reservations, setReservations] = useState<any[]>([]);
const [members, setMembers] = useState<any[]>([]);
const [themes, setThemes] = useState<any[]>([]);
const [times, setTimes] = useState<any[]>([]);
const [reservations, setReservations] = useState<ReservationRetrieveResponse[]>([]);
const [members, setMembers] = useState<MemberRetrieveResponse[]>([]);
const [themes, setThemes] = useState<ThemeRetrieveResponse[]>([]);
const [times, setTimes] = useState<TimeRetrieveResponse[]>([]);
const [isEditing, setIsEditing] = useState(false);
const [newReservation, setNewReservation] = useState({ memberId: '', themeId: '', date: '', timeId: '' });
const [filter, setFilter] = useState({ memberId: '', themeId: '', dateFrom: '', dateTo: '' });
const navigate = useNavigate();
const location = useLocation();
const handleError = (err: any) => {
if (isLoginRequiredError(err)) {
alert('로그인이 필요해요.');
navigate('/login', { state: { from: location } });
} else {
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
alert(message);
console.error(err);
}
};
useEffect(() => {
fetchReservations();
axios.get('/api/members').then(res => setMembers(res.data.data.members));
axios.get('/api/themes').then(res => setThemes(res.data.data.themes));
axios.get('/api/times').then(res => setTimes(res.data.data.times));
_fetchReservations();
fetchMembers().then(res => setMembers(res.members)).catch(handleError);
fetchThemes().then(res => setThemes(res.themes)).catch(handleError);
fetchTimes().then(res => setTimes(res.times)).catch(handleError);
}, []);
const fetchReservations = () => {
axios.get('/api/reservations')
.then(res => setReservations(res.data.data.reservations))
.catch(err => console.error(err));
const _fetchReservations = () => {
fetchReservations()
.then(res => setReservations(res.reservations))
.catch(handleError);
}
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
@ -29,28 +56,49 @@ const AdminReservationPage: React.FC = () => {
const applyFilter = (e: React.FormEvent) => {
e.preventDefault();
const queryParams = new URLSearchParams(filter).toString();
axios.get(`/api/reservations/search?${queryParams}`)
.then(res => setReservations(res.data.data.reservations))
.catch(err => console.error(err));
const params = {
memberId: filter.memberId ? Number(filter.memberId) : undefined,
themeId: filter.themeId ? Number(filter.themeId) : undefined,
dateFrom: filter.dateFrom,
dateTo: filter.dateTo,
};
searchReservations(params)
.then(res => setReservations(res.reservations))
.catch(handleError);
};
const handleAddClick = () => setIsEditing(true);
const handleCancelClick = () => setIsEditing(false);
const handleSaveClick = () => {
axios.post('/api/reservations/admin', newReservation)
const handleSaveClick = async () => {
if (!newReservation.memberId || !newReservation.themeId || !newReservation.date || !newReservation.timeId) {
alert('모든 필드를 입력해주세요.');
return;
}
const request = {
memberId: Number(newReservation.memberId),
themeId: Number(newReservation.themeId),
date: newReservation.date,
timeId: Number(newReservation.timeId),
};
await createReservationByAdmin(request)
.then(() => {
fetchReservations();
alert('예약을 추가했어요. 결제는 별도로 진행해주세요.');
_fetchReservations();
handleCancelClick();
})
.catch(err => alert('추가 실패: ' + err.response.data.message));
.catch(handleError);
};
const deleteReservation = (id: number) => {
axios.delete(`/api/reservations/${id}`)
.then(() => setReservations(reservations.filter(r => r.id !== id)))
.catch(err => alert('삭제 실패: ' + err.response.data.message));
const deleteReservation = async(id: number) => {
if (!window.confirm('정말 삭제하시겠어요?')) {
return;
}
await cancelReservationByAdmin(id)
.then(() => {
setReservations(reservations.filter(r => r.id !== id))
alert('예약을 삭제했어요.');
}).catch(handleError);
};
return (

View File

@ -1,15 +1,33 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { useLocation, useNavigate } from 'react-router-dom';
import { createTheme, fetchThemes, delTheme } from '@_api/theme/themeAPI';
import { isLoginRequiredError } from '@_api/apiClient';
const AdminThemePage: React.FC = () => {
const [themes, setThemes] = useState<any[]>([]);
const [isEditing, setIsEditing] = useState(false);
const [newTheme, setNewTheme] = useState({ name: '', description: '', thumbnail: '' });
const navigate = useNavigate();
const location = useLocation();
const handleError = (err: any) => {
if (isLoginRequiredError(err)) {
alert('로그인이 필요해요.');
navigate('/login', { state: { from: location } });
} else {
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
alert(message);
console.error(err);
}
};
useEffect(() => {
axios.get('/api/themes')
.then(res => setThemes(res.data.data.themes))
.catch(err => console.error('Error fetching themes:', err));
const fetchData = async () => {
await fetchThemes()
.then(response => setThemes(response.themes))
.catch(handleError);
};
fetchData();
}, []);
const handleAddClick = () => {
@ -21,21 +39,26 @@ const AdminThemePage: React.FC = () => {
setNewTheme({ name: '', description: '', thumbnail: '' });
};
const handleSaveClick = () => {
axios.post('/api/themes', newTheme)
.then(res => {
setThemes([...themes, res.data.data]);
const handleSaveClick = async () => {
await createTheme(newTheme)
.then((response) => {
setThemes([...themes, response]);
alert('테마를 추가했어요.');
handleCancelClick();
})
.catch(err => {
alert('테마 추가에 실패했습니다: ' + err.response.data.message);
});
};
.catch(handleError);
}
const deleteTheme = (id: number) => {
axios.delete(`/api/themes/${id}`)
.then(() => setThemes(themes.filter(t => t.id !== id)))
.catch(err => alert('삭제 실패: ' + err.response.data.message));
const deleteTheme = async (id: number) => {
if (!window.confirm('정말 삭제하시겠어요?')) {
return;
}
await delTheme(id)
.then(() => {
setThemes(themes.filter(theme => theme.id !== id));
alert('테마를 삭제했어요.');
})
.catch(handleError);
};
return (

View File

@ -1,15 +1,34 @@
import { createTime, delTime, fetchTimes } from '@_api/time/timeAPI';
import type { TimeCreateRequest } from '@_api/time/timeTypes';
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { useLocation, useNavigate } from 'react-router-dom';
import { isLoginRequiredError } from '@_api/apiClient';
const AdminTimePage: React.FC = () => {
const [times, setTimes] = useState<any[]>([]);
const [isEditing, setIsEditing] = useState(false);
const [newTime, setNewTime] = useState('');
const navigate = useNavigate();
const location = useLocation();
const handleError = (err: any) => {
if (isLoginRequiredError(err)) {
alert('로그인이 필요해요.');
navigate('/login', { state: { from: location } });
} else {
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
alert(message);
console.error(err);
}
};
useEffect(() => {
axios.get('/api/times')
.then(res => setTimes(res.data.data.times))
.catch(err => console.error('Error fetching times:', err));
const fetchData = async () => {
await fetchTimes()
.then(response => setTimes(response.times))
.catch(handleError);
}
fetchData();
}, []);
const handleAddClick = () => {
@ -21,21 +40,39 @@ const AdminTimePage: React.FC = () => {
setNewTime('');
};
const handleSaveClick = () => {
axios.post('/api/times', { startAt: newTime })
.then(res => {
setTimes([...times, res.data.data]);
const handleSaveClick = async () => {
if (!newTime) {
alert('시간을 입력해주세요.');
return;
}
if (!/^\d{2}:\d{2}$/.test(newTime)) {
alert('시간 형식이 올바르지 않습니다. HH:MM 형식으로 입력해주세요.');
return;
}
const request: TimeCreateRequest = {
startAt: newTime
};
await createTime(request)
.then((response) => {
setTimes([...times, response]);
alert('시간을 추가했어요.');
handleCancelClick();
})
.catch(err => {
alert('시간 추가에 실패했습니다: ' + err.response.data.message);
});
.catch(handleError);
};
const deleteTime = (id: number) => {
axios.delete(`/api/times/${id}`)
.then(() => setTimes(times.filter(t => t.id !== id)))
.catch(err => alert('삭제 실패: ' + err.response.data.message));
const deleteTime = async (id: number) => {
if (!window.confirm('정말 삭제하시겠어요?')) {
return;
}
await delTime(id)
.then(() => {
setTimes(times.filter(time => time.id !== id));
alert('시간을 삭제했어요.');
})
.catch(handleError);
};
return (
@ -79,4 +116,4 @@ const AdminTimePage: React.FC = () => {
);
};
export default AdminTimePage;
export default AdminTimePage;

View File

@ -1,31 +1,50 @@
import { confirmWaiting, fetchWaitingReservations, rejectWaiting } from '@_api/reservation/reservationAPI';
import type { ReservationRetrieveResponse } from '@_api/reservation/reservationTypes';
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { useLocation, useNavigate } from 'react-router-dom';
import { isLoginRequiredError } from '@_api/apiClient';
const AdminWaitingPage: React.FC = () => {
const [waitings, setWaitings] = useState<any[]>([]);
const [waitings, setWaitings] = useState<ReservationRetrieveResponse[]>([]);
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
axios.get('/api/reservations/waiting')
.then(res => setWaitings(res.data.data.reservations))
.catch(err => console.error(err));
}, []);
const approveWaiting = (id: number) => {
axios.post(`/api/reservations/waiting/${id}/confirm`)
.then(() => {
alert('승인되었습니다.');
setWaitings(waitings.filter(w => w.id !== id));
})
.catch(err => alert('승인 실패: ' + err.response.data.message));
const handleError = (err: any) => {
if (isLoginRequiredError(err)) {
alert('로그인이 필요해요.');
navigate('/login', { state: { from: location } });
} else {
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
alert(message);
console.error(err);
}
};
const denyWaiting = (id: number) => {
axios.post(`/api/reservations/waiting/${id}/reject`)
useEffect(() => {
const fetchData = async () => {
await fetchWaitingReservations()
.then(res => setWaitings(res.reservations))
.catch(handleError);
}
fetchData();
}, []);
const approveWaiting = async (id: number) => {
await confirmWaiting(id)
.then(() => {
alert('거절되었습니다.');
alert('대기 중인 예약을 승인했어요. 결제는 별도로 진행해주세요.');
setWaitings(waitings.filter(w => w.id !== id));
})
.catch(err => alert('거절 실패: ' + err.response.data.message));
.catch(handleError);
};
const denyWaiting = async (id: number) => {
await rejectWaiting(id)
.then(() => {
alert('대기 중인 예약을 거절했어요.');
setWaitings(waitings.filter(w => w.id !== id));
})
.catch(handleError);
};
return (
@ -63,4 +82,4 @@ const AdminWaitingPage: React.FC = () => {
);
};
export default AdminWaitingPage;
export default AdminWaitingPage;

View File

@ -8,13 +8,4 @@ export default defineConfig({
react(),
tsconfigPaths(),
],
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})