generated from pricelees/issue-pr-template
[#22] 프론트엔드 React 전환 및 인증 API 수정 #23
1
frontend/.env
Normal file
1
frontend/.env
Normal file
@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL = "http://localhost:8080"
|
||||
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@ -24,3 +24,5 @@ dist-ssr
|
||||
*.sw?
|
||||
|
||||
.DS_Store
|
||||
|
||||
.env
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
81
frontend/src/api/apiClient.ts
Normal file
81
frontend/src/api/apiClient.ts
Normal 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
|
||||
};
|
||||
19
frontend/src/api/auth/authAPI.ts
Normal file
19
frontend/src/api/auth/authAPI.ts
Normal 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');
|
||||
};
|
||||
14
frontend/src/api/auth/authTypes.ts
Normal file
14
frontend/src/api/auth/authTypes.ts
Normal 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';
|
||||
}
|
||||
|
||||
10
frontend/src/api/member/memberAPI.ts
Normal file
10
frontend/src/api/member/memberAPI.ts
Normal 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);
|
||||
};
|
||||
19
frontend/src/api/member/memberTypes.ts
Normal file
19
frontend/src/api/member/memberTypes.ts
Normal 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;
|
||||
}
|
||||
70
frontend/src/api/reservation/reservationAPI.ts
Normal file
70
frontend/src/api/reservation/reservationAPI.ts
Normal 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);
|
||||
};
|
||||
72
frontend/src/api/reservation/reservationTypes.ts
Normal file
72
frontend/src/api/reservation/reservationTypes.ts
Normal 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;
|
||||
}
|
||||
18
frontend/src/api/theme/themeAPI.ts
Normal file
18
frontend/src/api/theme/themeAPI.ts
Normal 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);
|
||||
};
|
||||
23
frontend/src/api/theme/themeTypes.ts
Normal file
23
frontend/src/api/theme/themeTypes.ts
Normal 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[];
|
||||
}
|
||||
18
frontend/src/api/time/timeAPI.ts
Normal file
18
frontend/src/api/time/timeAPI.ts
Normal 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);
|
||||
};
|
||||
27
frontend/src/api/time/timeTypes.ts
Normal file
27
frontend/src/api/time/timeTypes.ts
Normal 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[];
|
||||
}
|
||||
28
frontend/src/components/AdminRoute.tsx
Normal file
28
frontend/src/components/AdminRoute.tsx
Normal 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;
|
||||
@ -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">
|
||||
|
||||
73
frontend/src/context/AuthContext.tsx
Normal file
73
frontend/src/context/AuthContext.tsx
Normal 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;
|
||||
};
|
||||
@ -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 (
|
||||
|
||||
@ -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' }}>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -8,13 +8,4 @@ export default defineConfig({
|
||||
react(),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user