generated from pricelees/issue-pr-template
[#37] 테마 스키마 재정의 #38
@ -20,6 +20,10 @@ import MyReservationPageV2 from './pages/v2/MyReservationPageV2';
|
|||||||
import ReservationStep1PageV21 from './pages/v2/ReservationStep1PageV21';
|
import ReservationStep1PageV21 from './pages/v2/ReservationStep1PageV21';
|
||||||
import ReservationStep2PageV21 from './pages/v2/ReservationStep2PageV21';
|
import ReservationStep2PageV21 from './pages/v2/ReservationStep2PageV21';
|
||||||
import ReservationSuccessPageV21 from './pages/v2/ReservationSuccessPageV21';
|
import ReservationSuccessPageV21 from './pages/v2/ReservationSuccessPageV21';
|
||||||
|
import HomePageV2 from './pages/v2/HomePageV2';
|
||||||
|
import LoginPageV2 from './pages/v2/LoginPageV2';
|
||||||
|
import SignupPageV2 from './pages/v2/SignupPageV2';
|
||||||
|
import AdminThemeEditPage from './pages/admin/AdminThemeEditPage';
|
||||||
|
|
||||||
const AdminRoutes = () => (
|
const AdminRoutes = () => (
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
@ -28,6 +32,7 @@ const AdminRoutes = () => (
|
|||||||
<Route path="/reservation" element={<AdminReservationPage />} />
|
<Route path="/reservation" element={<AdminReservationPage />} />
|
||||||
<Route path="/time" element={<AdminTimePage />} />
|
<Route path="/time" element={<AdminTimePage />} />
|
||||||
<Route path="/theme" element={<AdminThemePage />} />
|
<Route path="/theme" element={<AdminThemePage />} />
|
||||||
|
<Route path="/theme/edit/:themeId" element={<AdminThemeEditPage />} />
|
||||||
<Route path="/waiting" element={<AdminWaitingPage />} />
|
<Route path="/waiting" element={<AdminWaitingPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
@ -53,6 +58,11 @@ function App() {
|
|||||||
<Route path="/my-reservation" element={<MyReservationPage />} />
|
<Route path="/my-reservation" element={<MyReservationPage />} />
|
||||||
<Route path="/my-reservation/v2" element={<MyReservationPageV2 />} />
|
<Route path="/my-reservation/v2" element={<MyReservationPageV2 />} />
|
||||||
|
|
||||||
|
{/* V2 Pages */}
|
||||||
|
<Route path="/v2/home" element={<HomePageV2 />} />
|
||||||
|
<Route path="/v2/login" element={<LoginPageV2 />} />
|
||||||
|
<Route path="/v2/signup" element={<SignupPageV2 />} />
|
||||||
|
|
||||||
{/* V2 Reservation Flow */}
|
{/* V2 Reservation Flow */}
|
||||||
<Route path="/v2/reservation" element={<ReservationStep1Page />} />
|
<Route path="/v2/reservation" element={<ReservationStep1Page />} />
|
||||||
<Route path="/v2/reservation/payment" element={<ReservationStep2Page />} />
|
<Route path="/v2/reservation/payment" element={<ReservationStep2Page />} />
|
||||||
|
|||||||
@ -28,7 +28,7 @@ async function request<T>(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isRequiredAuth) {
|
|
||||||
const accessToken = localStorage.getItem('accessToken');
|
const accessToken = localStorage.getItem('accessToken');
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
if (!config.headers) {
|
if (!config.headers) {
|
||||||
@ -36,7 +36,7 @@ async function request<T>(
|
|||||||
}
|
}
|
||||||
config.headers['Authorization'] = `Bearer ${accessToken}`;
|
config.headers['Authorization'] = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (method.toUpperCase() !== 'GET') {
|
if (method.toUpperCase() !== 'GET') {
|
||||||
config.data = data;
|
config.data = data;
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
import apiClient from "@_api/apiClient";
|
import apiClient from '@_api/apiClient';
|
||||||
import type { ThemeCreateRequest, ThemeCreateResponse, ThemeRetrieveListResponse } from "./themeTypes";
|
import type {
|
||||||
|
AdminThemeDetailRetrieveResponse,
|
||||||
|
AdminThemeSummaryRetrieveListResponse,
|
||||||
|
ThemeCreateRequest,
|
||||||
|
ThemeCreateRequestV2, ThemeCreateResponse,
|
||||||
|
ThemeCreateResponseV2, ThemeRetrieveListResponse,
|
||||||
|
ThemeUpdateRequest,
|
||||||
|
UserThemeRetrieveListResponse
|
||||||
|
} from './themeTypes';
|
||||||
|
|
||||||
export const createTheme = async (data: ThemeCreateRequest): Promise<ThemeCreateResponse> => {
|
export const createTheme = async (data: ThemeCreateRequest): Promise<ThemeCreateResponse> => {
|
||||||
return await apiClient.post<ThemeCreateResponse>('/themes', data, true);
|
return await apiClient.post<ThemeCreateResponse>('/themes', data, true);
|
||||||
@ -16,3 +24,27 @@ export const mostReservedThemes = async (count: number = 10): Promise<ThemeRetri
|
|||||||
export const delTheme = async (id: string): Promise<void> => {
|
export const delTheme = async (id: string): Promise<void> => {
|
||||||
return await apiClient.del(`/themes/${id}`, true);
|
return await apiClient.del(`/themes/${id}`, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchAdminThemes = async (): Promise<AdminThemeSummaryRetrieveListResponse> => {
|
||||||
|
return await apiClient.get<AdminThemeSummaryRetrieveListResponse>('/admin/themes');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchAdminThemeDetail = async (id: string): Promise<AdminThemeDetailRetrieveResponse> => {
|
||||||
|
return await apiClient.get<AdminThemeDetailRetrieveResponse>(`/admin/themes/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createThemeV2 = async (themeData: ThemeCreateRequestV2): Promise<ThemeCreateResponseV2> => {
|
||||||
|
return await apiClient.post<ThemeCreateResponseV2>('/admin/themes', themeData);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTheme = async (id: string, themeData: ThemeUpdateRequest): Promise<void> => {
|
||||||
|
await apiClient.patch<any>(`/admin/themes/${id}`, themeData);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteTheme = async (id: string): Promise<void> => {
|
||||||
|
await apiClient.del<any>(`/admin/themes/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchUserThemes = async (): Promise<UserThemeRetrieveListResponse> => {
|
||||||
|
return await apiClient.get<UserThemeRetrieveListResponse>('/v2/themes');
|
||||||
|
};
|
||||||
|
|||||||
@ -21,3 +21,113 @@ export interface ThemeRetrieveResponse {
|
|||||||
export interface ThemeRetrieveListResponse {
|
export interface ThemeRetrieveListResponse {
|
||||||
themes: ThemeRetrieveResponse[];
|
themes: ThemeRetrieveResponse[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface ThemeV2 {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
difficulty: Difficulty;
|
||||||
|
price: number;
|
||||||
|
minParticipants: number;
|
||||||
|
maxParticipants: number;
|
||||||
|
availableMinutes: number;
|
||||||
|
expectedMinutesFrom: number;
|
||||||
|
expectedMinutesTo: number;
|
||||||
|
isOpen: boolean;
|
||||||
|
createDate: string; // Assuming ISO string format
|
||||||
|
updatedDate: string; // Assuming ISO string format
|
||||||
|
createdBy: string;
|
||||||
|
updatedBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeCreateRequestV2 {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
difficulty: Difficulty;
|
||||||
|
price: number;
|
||||||
|
minParticipants: number;
|
||||||
|
maxParticipants: number;
|
||||||
|
availableMinutes: number;
|
||||||
|
expectedMinutesFrom: number;
|
||||||
|
expectedMinutesTo: number;
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeCreateResponseV2 {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeUpdateRequest {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
thumbnailUrl?: string;
|
||||||
|
difficulty?: Difficulty;
|
||||||
|
price?: number;
|
||||||
|
minParticipants?: number;
|
||||||
|
maxParticipants?: number;
|
||||||
|
availableMinutes?: number;
|
||||||
|
expectedMinutesFrom?: number;
|
||||||
|
expectedMinutesTo?: number;
|
||||||
|
isOpen?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminThemeSummaryRetrieveResponse {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
difficulty: Difficulty;
|
||||||
|
price: number;
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminThemeSummaryRetrieveListResponse {
|
||||||
|
themes: AdminThemeSummaryRetrieveResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminThemeDetailRetrieveResponse {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
difficulty: Difficulty;
|
||||||
|
price: number;
|
||||||
|
minParticipants: number;
|
||||||
|
maxParticipants: number;
|
||||||
|
availableMinutes: number;
|
||||||
|
expectedMinutesFrom: number;
|
||||||
|
expectedMinutesTo: number;
|
||||||
|
isOpen: boolean;
|
||||||
|
createdAt: string; // LocalDateTime in Kotlin, map to string (ISO format)
|
||||||
|
createdBy: string;
|
||||||
|
updatedAt: string; // LocalDateTime in Kotlin, map to string (ISO format)
|
||||||
|
updatedBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserThemeRetrieveResponse {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
description: string;
|
||||||
|
difficulty: Difficulty;
|
||||||
|
price: number;
|
||||||
|
minParticipants: number;
|
||||||
|
maxParticipants: number;
|
||||||
|
availableMinutes: number;
|
||||||
|
expectedMinutesFrom: number;
|
||||||
|
expectedMinutesTo: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserThemeRetrieveListResponse {
|
||||||
|
themes: UserThemeRetrieveResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
export enum Difficulty {
|
||||||
|
VERY_EASY = 'VERY_EASY',
|
||||||
|
EASY = 'EASY',
|
||||||
|
NORMAL = 'NORMAL',
|
||||||
|
HARD = 'HARD',
|
||||||
|
VERY_HARD = 'VERY_HARD',
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from 'src/context/AuthContext';
|
import { useAuth } from 'src/context/AuthContext';
|
||||||
|
import 'src/css/navbar.css';
|
||||||
|
|
||||||
const Navbar: React.FC = () => {
|
const Navbar: React.FC = () => {
|
||||||
const { loggedIn, userName, logout } = useAuth();
|
const { loggedIn, userName, logout } = useAuth();
|
||||||
@ -14,39 +15,31 @@ const Navbar: React.FC = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Logout failed:', error);
|
console.error('Logout failed:', error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="navbar navbar-expand-lg navbar-light bg-light">
|
<nav className="navbar-container">
|
||||||
<Link className="navbar-brand" to="/">
|
<div className="nav-links">
|
||||||
<img src="/image/service-logo.png" alt="LOGO" style={{ height: '40px' }} />
|
<Link className="nav-link" to="/">홈</Link>
|
||||||
</Link>
|
<Link className="nav-link" to="/v2/reservation">예약하기</Link>
|
||||||
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
</div>
|
||||||
<span className="navbar-toggler-icon"></span>
|
<div className="nav-actions">
|
||||||
</button>
|
|
||||||
<div className="collapse navbar-collapse" id="navbarSupportedContent">
|
|
||||||
<ul className="navbar-nav ms-auto">
|
|
||||||
<li className="nav-item">
|
|
||||||
<Link className="nav-link" to="/v2/reservation">Reservation</Link>
|
|
||||||
</li>
|
|
||||||
{!loggedIn ? (
|
{!loggedIn ? (
|
||||||
<li className="nav-item">
|
<>
|
||||||
<Link className="nav-link" to="/login">Login</Link>
|
<button className="btn btn-secondary" onClick={() => navigate('/v2/login')}>로그인</button>
|
||||||
</li>
|
<button className="btn btn-primary" onClick={() => navigate('/v2/signup')}>회원가입</button>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<li className="nav-item dropdown">
|
<div className="profile-info">
|
||||||
<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" />
|
<img className="profile-image" src="/image/default-profile.png" alt="Profile" />
|
||||||
<span id="profile-name">{userName}</span>
|
<span>{userName}</span>
|
||||||
</a>
|
<div className="dropdown-menu">
|
||||||
<ul className="dropdown-menu" aria-labelledby="navbarDropdown">
|
<Link className="dropdown-item" to="/my-reservation/v2">내 예약</Link>
|
||||||
<li><Link className="dropdown-item" to="/my-reservation/v2">My Reservation</Link></li>
|
<div className="dropdown-divider" />
|
||||||
<li><hr className="dropdown-divider" /></li>
|
<a className="dropdown-item" href="#" onClick={handleLogout}>로그아웃</a>
|
||||||
<li><a className="dropdown-item" href="#" onClick={handleLogout}>Logout</a></li>
|
</div>
|
||||||
</ul>
|
</div>
|
||||||
</li>
|
|
||||||
)}
|
)}
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
|
|||||||
17
frontend/src/css/admin-page.css
Normal file
17
frontend/src/css/admin-page.css
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/* /src/css/admin-page.css */
|
||||||
|
.admin-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 40px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background-color: #f4f6f8;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-container .page-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333d4b;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
160
frontend/src/css/admin-reservation-page.css
Normal file
160
frontend/src/css/admin-reservation-page.css
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
/* /src/css/admin-reservation-page.css */
|
||||||
|
.admin-reservation-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 40px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background-color: #f4f6f8;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-reservation-container .page-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333d4b;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-reservation-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reservations-main {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
width: 300px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card .card-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333d4b;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container th,
|
||||||
|
.table-container td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e5e8eb;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container th {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
color: #505a67;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container tr:hover {
|
||||||
|
background-color: #f4f6f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4E5968;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input,
|
||||||
|
.form-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
border: 1px solid #E5E8EB;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus,
|
||||||
|
.form-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3182F6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(49, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #3182F6;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #1B64DA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #F2F4F6;
|
||||||
|
color: #4E5968;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: #E5E8EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: #e53e3e;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background-color: #c53030;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section .btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editing-row td {
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editing-row .btn {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
236
frontend/src/css/admin-theme-edit-page.css
Normal file
236
frontend/src/css/admin-theme-edit-page.css
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
:root {
|
||||||
|
--primary-color: #007bff;
|
||||||
|
--secondary-color: #6c757d;
|
||||||
|
--danger-color: #dc3545;
|
||||||
|
--light-gray-color: #f8f9fa;
|
||||||
|
--dark-gray-color: #343a40;
|
||||||
|
--border-color: #dee2e6;
|
||||||
|
--input-bg-color: #fff;
|
||||||
|
--text-color: #212529;
|
||||||
|
--label-color: #495057;
|
||||||
|
--white-color: #ffffff;
|
||||||
|
--box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
--border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-theme-edit-container {
|
||||||
|
padding: 2rem 0;
|
||||||
|
background-color: var(--light-gray-color);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.centered-layout {
|
||||||
|
max-width: 1024px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.centered-layout {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--dark-gray-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
background-color: var(--white-color);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group.full-width {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--label-color);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input,
|
||||||
|
.form-textarea,
|
||||||
|
.form-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--input-bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
height: 3rem; /* 48px */
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:disabled,
|
||||||
|
.form-textarea:disabled,
|
||||||
|
.form-select:disabled {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus,
|
||||||
|
.form-textarea:focus,
|
||||||
|
.form-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
height: auto;
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s, transform 0.1s;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-actions .btn {
|
||||||
|
padding: 0.85rem 2.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--white-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: var(--white-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: var(--danger-color);
|
||||||
|
color: var(--white-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-info {
|
||||||
|
background-color: var(--white-color);
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--dark-gray-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-body p {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--label-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-body p strong {
|
||||||
|
color: var(--dark-gray-color);
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-section {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete-text {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--danger-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete-text:hover {
|
||||||
|
color: #a71d2a;
|
||||||
|
}
|
||||||
121
frontend/src/css/admin-theme-page.css
Normal file
121
frontend/src/css/admin-theme-page.css
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/* /src/css/admin-theme-page.css */
|
||||||
|
.admin-theme-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 40px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background-color: #f4f6f8;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-theme-container .page-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333d4b;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container th,
|
||||||
|
.table-container td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e5e8eb;
|
||||||
|
vertical-align: middle;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container th {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
color: #505a67;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container tr:hover {
|
||||||
|
background-color: #f4f6f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
border: 1px solid #E5E8EB;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3182F6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(49, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #3182F6;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #1B64DA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #F2F4F6;
|
||||||
|
color: #4E5968;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: #E5E8EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: #e53e3e;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background-color: #c53030;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editing-row td {
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editing-row .btn {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
120
frontend/src/css/admin-time-page.css
Normal file
120
frontend/src/css/admin-time-page.css
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
/* /src/css/admin-time-page.css */
|
||||||
|
.admin-time-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 40px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background-color: #f4f6f8;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-time-container .page-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333d4b;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container th,
|
||||||
|
.table-container td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e5e8eb;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container th {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
color: #505a67;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container tr:hover {
|
||||||
|
background-color: #f4f6f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
border: 1px solid #E5E8EB;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3182F6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(49, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #3182F6;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #1B64DA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #F2F4F6;
|
||||||
|
color: #4E5968;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: #E5E8EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: #e53e3e;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background-color: #c53030;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editing-row td {
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editing-row .btn {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
81
frontend/src/css/admin-waiting-page.css
Normal file
81
frontend/src/css/admin-waiting-page.css
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/* /src/css/admin-waiting-page.css */
|
||||||
|
.admin-waiting-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 40px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background-color: #f4f6f8;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-waiting-container .page-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333d4b;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container th,
|
||||||
|
.table-container td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e5e8eb;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container th {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
color: #505a67;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container tr:hover {
|
||||||
|
background-color: #f4f6f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #3182F6;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #1B64DA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: #e53e3e;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background-color: #c53030;
|
||||||
|
}
|
||||||
66
frontend/src/css/home-page-v2.css
Normal file
66
frontend/src/css/home-page-v2.css
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/* /src/css/home-page-v2.css */
|
||||||
|
.home-container-v2 {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background-color: #f4f6f8;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-container-v2 .page-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333d4b;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-ranking-list-v2 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-ranking-item-v2 {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-ranking-item-v2:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-ranking-item-v2 .thumbnail {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e5e8eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-ranking-item-v2 .theme-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-ranking-item-v2 .theme-name {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333d4b;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-ranking-item-v2 .theme-description {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #505a67;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
74
frontend/src/css/login-page-v2.css
Normal file
74
frontend/src/css/login-page-v2.css
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/* /src/css/login-page-v2.css */
|
||||||
|
.login-container-v2 {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 80px auto;
|
||||||
|
padding: 40px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.07);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container-v2 .page-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #191F28;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-v2 .form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-v2 .form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 1px solid #E5E8EB;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-v2 .form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3182F6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(49, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-v2 .button-group {
|
||||||
|
margin-top: 30px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-v2 .btn {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 14px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-v2 .btn-primary {
|
||||||
|
background-color: #3182F6;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-v2 .btn-primary:hover {
|
||||||
|
background-color: #1B64DA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-v2 .btn-secondary {
|
||||||
|
background-color: #F2F4F6;
|
||||||
|
color: #4E5968;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-v2 .btn-secondary:hover {
|
||||||
|
background-color: #E5E8EB;
|
||||||
|
}
|
||||||
117
frontend/src/css/navbar.css
Normal file
117
frontend/src/css/navbar.css
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
/* /src/css/navbar.css */
|
||||||
|
.navbar-container {
|
||||||
|
background-color: #ffffff;
|
||||||
|
height: 60px;
|
||||||
|
padding: 0 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid #e5e8eb;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .nav-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .nav-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #4E5968;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .nav-link:hover {
|
||||||
|
color: #191F28;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .nav-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .btn-primary {
|
||||||
|
background-color: #3182F6;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .btn-primary:hover {
|
||||||
|
background-color: #1B64DA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .btn-secondary {
|
||||||
|
background-color: #F2F4F6;
|
||||||
|
color: #4E5968;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .btn-secondary:hover {
|
||||||
|
background-color: #E5E8EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .profile-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #333d4b;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .profile-image {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .dropdown-menu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
min-width: 160px;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .profile-info:hover .dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .dropdown-item {
|
||||||
|
display: block;
|
||||||
|
padding: 10px 16px;
|
||||||
|
color: #333d4b;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .dropdown-item:hover {
|
||||||
|
background-color: #f4f6f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container .dropdown-divider {
|
||||||
|
height: 1px;
|
||||||
|
margin: 8px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #e5e8eb;
|
||||||
|
}
|
||||||
65
frontend/src/css/signup-page-v2.css
Normal file
65
frontend/src/css/signup-page-v2.css
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/* /src/css/signup-page-v2.css */
|
||||||
|
.signup-container-v2 {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 80px auto;
|
||||||
|
padding: 40px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.07);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-container-v2 .page-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #191F28;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-form-v2 .form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-form-v2 .form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4E5968;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-form-v2 .form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 1px solid #E5E8EB;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-form-v2 .form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3182F6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(49, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-form-v2 .btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 14px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #3182F6;
|
||||||
|
color: #ffffff;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-form-v2 .btn-primary:hover {
|
||||||
|
background-color: #1B64DA;
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import type { LoginRequest } from '@_api/auth/authTypes';
|
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
|
||||||
const LoginPage: React.FC = () => {
|
const LoginPage: React.FC = () => {
|
||||||
@ -14,8 +13,7 @@ const LoginPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
try {
|
try {
|
||||||
const request: LoginRequest = { email, password };
|
await login({email, password});
|
||||||
await login(request);
|
|
||||||
|
|
||||||
alert('로그인에 성공했어요!');
|
alert('로그인에 성공했어요!');
|
||||||
navigate(from, { replace: true });
|
navigate(from, { replace: true });
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
|
import '../../css/navbar.css';
|
||||||
|
|
||||||
const AdminNavbar: React.FC = () => {
|
const AdminNavbar: React.FC = () => {
|
||||||
const { loggedIn, userName, logout } = useAuth();
|
const { loggedIn, userName, logout } = useAuth();
|
||||||
@ -13,48 +14,30 @@ const AdminNavbar: React.FC = () => {
|
|||||||
navigate('/');
|
navigate('/');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Logout failed", error);
|
console.error("Logout failed", error);
|
||||||
// Handle logout error if needed
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="navbar navbar-expand-lg navbar-light bg-light">
|
<nav className="navbar-container">
|
||||||
<Link className="navbar-brand" to="/admin">
|
<div className="nav-links">
|
||||||
<img src="/image/admin-logo.png" alt="LOGO" style={{ height: '40px' }} />
|
<Link className="nav-link" to="/admin">홈</Link>
|
||||||
</Link>
|
<Link className="nav-link" to="/admin/reservation">예약</Link>
|
||||||
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
<Link className="nav-link" to="/admin/waiting">대기</Link>
|
||||||
<span className="navbar-toggler-icon"></span>
|
<Link className="nav-link" to="/admin/theme">테마</Link>
|
||||||
</button>
|
<Link className="nav-link" to="/admin/time">시간</Link>
|
||||||
<div className="collapse navbar-collapse" id="navbarSupportedContent">
|
</div>
|
||||||
<ul className="navbar-nav ms-auto">
|
<div className="nav-actions">
|
||||||
<li className="nav-item">
|
|
||||||
<Link className="nav-link" to="/admin/reservation">Reservation</Link>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item">
|
|
||||||
<Link className="nav-link" to="/admin/waiting">Waiting</Link>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item">
|
|
||||||
<Link className="nav-link" to="/admin/theme">Theme</Link>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item">
|
|
||||||
<Link className="nav-link" to="/admin/time">Time</Link>
|
|
||||||
</li>
|
|
||||||
{!loggedIn ? (
|
{!loggedIn ? (
|
||||||
<li className="nav-item">
|
<button className="btn btn-primary" onClick={() => navigate('/v2/login')}>Login</button>
|
||||||
<Link className="nav-link" to="/login">Login</Link>
|
|
||||||
</li>
|
|
||||||
) : (
|
) : (
|
||||||
<li className="nav-item dropdown">
|
<div className="profile-info">
|
||||||
<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" />
|
<img className="profile-image" src="/image/default-profile.png" alt="Profile" />
|
||||||
<span id="profile-name">{userName || 'Profile'}</span>
|
<span>{userName || 'Profile'}</span>
|
||||||
</a>
|
<div className="dropdown-menu">
|
||||||
<ul className="dropdown-menu" aria-labelledby="navbarDropdown">
|
<a className="dropdown-item" href="#" onClick={handleLogout}>Logout</a>
|
||||||
<li><a className="dropdown-item" href="#" onClick={handleLogout}>Logout</a></li>
|
</div>
|
||||||
</ul>
|
</div>
|
||||||
</li>
|
|
||||||
)}
|
)}
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import '../../css/admin-page.css';
|
||||||
|
|
||||||
const AdminPage: React.FC = () => {
|
const AdminPage: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="content-container">
|
<div className="admin-container">
|
||||||
<h2 className="content-container-title">방탈출 어드민</h2>
|
<h2 className="page-title">방탈출 어드민</h2>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
266
frontend/src/pages/admin/AdminThemeEditPage.tsx
Normal file
266
frontend/src/pages/admin/AdminThemeEditPage.tsx
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
import { isLoginRequiredError } from '@_api/apiClient';
|
||||||
|
import {
|
||||||
|
createThemeV2,
|
||||||
|
deleteTheme,
|
||||||
|
fetchAdminThemeDetail,
|
||||||
|
updateTheme
|
||||||
|
} from '@_api/theme/themeAPI';
|
||||||
|
import { Difficulty, type ThemeCreateRequestV2, type ThemeUpdateRequest, type ThemeV2 } from '@_api/theme/themeTypes';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import '../../css/admin-theme-edit-page.css';
|
||||||
|
|
||||||
|
const AdminThemeEditPage: React.FC = () => {
|
||||||
|
const { themeId } = useParams<{ themeId: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isNew = themeId === 'new';
|
||||||
|
|
||||||
|
const [theme, setTheme] = useState<ThemeV2 | ThemeCreateRequestV2 | null>(null);
|
||||||
|
const [originalTheme, setOriginalTheme] = useState<ThemeV2 | ThemeCreateRequestV2 | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isEditing, setIsEditing] = useState(isNew);
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
if (isNew) {
|
||||||
|
const newTheme: ThemeCreateRequestV2 = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
thumbnailUrl: '',
|
||||||
|
difficulty: Difficulty.NORMAL,
|
||||||
|
price: 0,
|
||||||
|
minParticipants: 2,
|
||||||
|
maxParticipants: 4,
|
||||||
|
availableMinutes: 60,
|
||||||
|
expectedMinutesFrom: 50,
|
||||||
|
expectedMinutesTo: 70,
|
||||||
|
isOpen: true,
|
||||||
|
};
|
||||||
|
setTheme(newTheme);
|
||||||
|
setOriginalTheme(newTheme);
|
||||||
|
setIsLoading(false);
|
||||||
|
} else if (themeId) {
|
||||||
|
fetchAdminThemeDetail(themeId)
|
||||||
|
.then(data => {
|
||||||
|
// Map AdminThemeDetailRetrieveResponse to ThemeV2
|
||||||
|
const fetchedTheme: ThemeV2 = {
|
||||||
|
id: data.id,
|
||||||
|
name: data.name,
|
||||||
|
description: data.description,
|
||||||
|
thumbnailUrl: data.thumbnailUrl,
|
||||||
|
difficulty: data.difficulty,
|
||||||
|
price: data.price,
|
||||||
|
minParticipants: data.minParticipants,
|
||||||
|
maxParticipants: data.maxParticipants,
|
||||||
|
availableMinutes: data.availableMinutes,
|
||||||
|
expectedMinutesFrom: data.expectedMinutesFrom,
|
||||||
|
expectedMinutesTo: data.expectedMinutesTo,
|
||||||
|
isOpen: data.isOpen,
|
||||||
|
createDate: data.createdAt, // Map createdAt to createDate
|
||||||
|
updatedDate: data.updatedAt, // Map updatedAt to updatedDate
|
||||||
|
createdBy: data.createdBy,
|
||||||
|
updatedBy: data.updatedBy,
|
||||||
|
};
|
||||||
|
setTheme(fetchedTheme);
|
||||||
|
setOriginalTheme(fetchedTheme);
|
||||||
|
})
|
||||||
|
.catch(handleError)
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}
|
||||||
|
}, [themeId, isNew, navigate]);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||||
|
const { name, value, type } = e.target;
|
||||||
|
let processedValue: string | number | boolean = value;
|
||||||
|
|
||||||
|
if (name === 'isOpen') {
|
||||||
|
processedValue = value === 'true';
|
||||||
|
} else if (type === 'checkbox') {
|
||||||
|
processedValue = (e.target as HTMLInputElement).checked;
|
||||||
|
} else if (type === 'number') {
|
||||||
|
processedValue = value === '' ? '' : Number(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(prev => prev ? { ...prev, [name]: processedValue } : null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelEdit = () => {
|
||||||
|
if (!isNew) {
|
||||||
|
setTheme(originalTheme);
|
||||||
|
setIsEditing(false);
|
||||||
|
} else {
|
||||||
|
navigate('/admin/theme');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
console.log('handleSubmit called');
|
||||||
|
e.preventDefault();
|
||||||
|
if (!theme) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isNew) {
|
||||||
|
await createThemeV2(theme as ThemeCreateRequestV2);
|
||||||
|
alert('테마가 성공적으로 생성되었습니다.');
|
||||||
|
navigate(`/admin/theme`);
|
||||||
|
} else {
|
||||||
|
if (!themeId) {
|
||||||
|
throw new Error('themeId is undefined');
|
||||||
|
}
|
||||||
|
await updateTheme(themeId, theme as ThemeUpdateRequest);
|
||||||
|
alert('테마가 성공적으로 업데이트되었습니다.');
|
||||||
|
setOriginalTheme(theme);
|
||||||
|
setIsEditing(false);
|
||||||
|
navigate(`/admin/theme`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (isNew || !themeId) return;
|
||||||
|
if (window.confirm('정말로 이 테마를 삭제하시겠습니까?')) {
|
||||||
|
try {
|
||||||
|
await deleteTheme(themeId);
|
||||||
|
alert('테마가 삭제되었습니다.');
|
||||||
|
navigate('/admin/theme');
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div className="admin-theme-edit-container"><p>로딩 중...</p></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!theme) {
|
||||||
|
return <div className="admin-theme-edit-container"><p>테마 정보를 찾을 수 없습니다.</p></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="admin-theme-edit-container">
|
||||||
|
<div className="centered-layout">
|
||||||
|
<header className="page-header">
|
||||||
|
<h2 className="page-title">{isNew ? '새 테마 추가' : '테마 정보 수정'}</h2>
|
||||||
|
</header>
|
||||||
|
<form onSubmit={handleSubmit} className="form-card">
|
||||||
|
<div className="form-section">
|
||||||
|
<div className="form-group full-width">
|
||||||
|
<label className="form-label" htmlFor="name">테마 이름</label>
|
||||||
|
<input id="name" name="name" type="text" className="form-input" value={theme.name} onChange={handleChange} required disabled={!isEditing} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group full-width">
|
||||||
|
<label className="form-label" htmlFor="description">설명</label>
|
||||||
|
<textarea id="description" name="description" className="form-textarea" value={theme.description} onChange={handleChange} required disabled={!isEditing} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group full-width">
|
||||||
|
<label className="form-label" htmlFor="thumbnailUrl">썸네일 URL</label>
|
||||||
|
<input id="thumbnailUrl" name="thumbnailUrl" type="text" className="form-input" value={theme.thumbnailUrl} onChange={handleChange} required disabled={!isEditing} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-section">
|
||||||
|
<div className="form-row">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label" htmlFor="difficulty">난이도</label>
|
||||||
|
<select id="difficulty" name="difficulty" className="form-select" value={theme.difficulty} onChange={handleChange} disabled={!isEditing}>
|
||||||
|
{Object.values(Difficulty).map(d => <option key={d} value={d}>{d}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label" htmlFor="isOpen">공개 여부</label>
|
||||||
|
<select id="isOpen" name="isOpen" className="form-select" value={String(theme.isOpen)} onChange={handleChange} disabled={!isEditing}>
|
||||||
|
<option value="true">공개</option>
|
||||||
|
<option value="false">비공개</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="form-row">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label" htmlFor="price">가격 (원)</label>
|
||||||
|
<input id="price" name="price" type="number" className="form-input" value={theme.price} onChange={handleChange} required disabled={!isEditing} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label" htmlFor="availableMinutes">총 이용시간 (분)</label>
|
||||||
|
<input id="availableMinutes" name="availableMinutes" type="number" className="form-input" value={theme.availableMinutes} onChange={handleChange} required disabled={!isEditing} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="form-row">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label" htmlFor="expectedMinutesFrom">최소 예상 시간 (분)</label>
|
||||||
|
<input id="expectedMinutesFrom" name="expectedMinutesFrom" type="number" className="form-input" value={theme.expectedMinutesFrom} onChange={handleChange} required disabled={!isEditing} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label" htmlFor="expectedMinutesTo">최대 예상 시간 (분)</label>
|
||||||
|
<input id="expectedMinutesTo" name="expectedMinutesTo" type="number" className="form-input" value={theme.expectedMinutesTo} onChange={handleChange} required disabled={!isEditing} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-row">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label" htmlFor="minParticipants">최소 인원 (명)</label>
|
||||||
|
<input id="minParticipants" name="minParticipants" type="number" className="form-input" value={theme.minParticipants} onChange={handleChange} required disabled={!isEditing} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label" htmlFor="maxParticipants">최대 인원 (명)</label>
|
||||||
|
<input id="maxParticipants" name="maxParticipants" type="number" className="form-input" value={theme.maxParticipants} onChange={handleChange} required disabled={!isEditing} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="button-group">
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="main-actions">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={handleCancelEdit}>취소</button>
|
||||||
|
<button type="submit" className="btn btn-primary">저장</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="main-actions">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => navigate('/admin/theme')}>목록</button>
|
||||||
|
<button type="button" className="btn btn-primary" onClick={(e) => { e.preventDefault(); console.log('setIsEditing(true) called'); setIsEditing(true); }}>수정</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{!isNew && 'id' in theme && (
|
||||||
|
<div className="audit-info">
|
||||||
|
<h4 className="audit-title">감사 정보</h4>
|
||||||
|
<div className="audit-body">
|
||||||
|
<p><strong>생성일:</strong> {new Date(theme.createDate).toLocaleString()}</p>
|
||||||
|
<p><strong>수정일:</strong> {new Date(theme.updatedDate).toLocaleString()}</p>
|
||||||
|
<p><strong>생성자:</strong> {theme.createdBy}</p>
|
||||||
|
<p><strong>수정자:</strong> {theme.updatedBy}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isNew && !isEditing && (
|
||||||
|
<div className="delete-section">
|
||||||
|
<button className="btn-delete-text" onClick={handleDelete}>이 테마를 삭제합니다.</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminThemeEditPage;
|
||||||
@ -14,6 +14,7 @@ import type { MemberRetrieveResponse } from '@_api/member/memberTypes';
|
|||||||
import type { ThemeRetrieveResponse } from '@_api/theme/themeTypes';
|
import type { ThemeRetrieveResponse } from '@_api/theme/themeTypes';
|
||||||
import type { TimeRetrieveResponse } from '@_api/time/timeTypes';
|
import type { TimeRetrieveResponse } from '@_api/time/timeTypes';
|
||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
import { isLoginRequiredError } from '@_api/apiClient';
|
||||||
|
import '../../css/admin-reservation-page.css';
|
||||||
|
|
||||||
const AdminReservationPage: React.FC = () => {
|
const AdminReservationPage: React.FC = () => {
|
||||||
const [reservations, setReservations] = useState<ReservationRetrieveResponse[]>([]);
|
const [reservations, setReservations] = useState<ReservationRetrieveResponse[]>([]);
|
||||||
@ -102,14 +103,15 @@ const AdminReservationPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content-container">
|
<div className="admin-reservation-container">
|
||||||
<h2 className="content-container-title">방탈출 예약 페이지</h2>
|
<h2 className="page-title">예약 관리</h2>
|
||||||
<div className="d-flex">
|
<div className="admin-reservation-content">
|
||||||
<div className="table-container flex-grow-1 mr-3">
|
<div className="reservations-main section-card">
|
||||||
<div className="table-header d-flex justify-content-end">
|
<div className="table-header">
|
||||||
<button id="add-button" className="btn btn-custom mb-2" onClick={handleAddClick}>예약 추가</button>
|
<button className="btn btn-primary" onClick={handleAddClick}>예약 추가</button>
|
||||||
</div>
|
</div>
|
||||||
<table className="table">
|
<div className="table-container">
|
||||||
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>예약번호</th>
|
<th>예약번호</th>
|
||||||
@ -117,7 +119,7 @@ const AdminReservationPage: React.FC = () => {
|
|||||||
<th>테마</th>
|
<th>테마</th>
|
||||||
<th>날짜</th>
|
<th>날짜</th>
|
||||||
<th>시간</th>
|
<th>시간</th>
|
||||||
<th>결제 완료 여부</th>
|
<th>상태</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -134,30 +136,30 @@ const AdminReservationPage: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<tr>
|
<tr className="editing-row">
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>
|
<td>
|
||||||
<select className="form-control" onChange={e => setNewReservation({ ...newReservation, memberId: e.target.value })}>
|
<select className="form-select" onChange={e => setNewReservation({ ...newReservation, memberId: e.target.value })}>
|
||||||
<option value="">멤버 선택</option>
|
<option value="">멤버 선택</option>
|
||||||
{members.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
|
{members.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select className="form-control" onChange={e => setNewReservation({ ...newReservation, themeId: e.target.value })}>
|
<select className="form-select" onChange={e => setNewReservation({ ...newReservation, themeId: e.target.value })}>
|
||||||
<option value="">테마 선택</option>
|
<option value="">테마 선택</option>
|
||||||
{themes.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
|
{themes.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td><input type="date" className="form-control" onChange={e => setNewReservation({ ...newReservation, date: e.target.value })} /></td>
|
<td><input type="date" className="form-input" onChange={e => setNewReservation({ ...newReservation, date: e.target.value })} /></td>
|
||||||
<td>
|
<td>
|
||||||
<select className="form-control" onChange={e => setNewReservation({ ...newReservation, timeId: e.target.value })}>
|
<select className="form-select" onChange={e => setNewReservation({ ...newReservation, timeId: e.target.value })}>
|
||||||
<option value="">시간 선택</option>
|
<option value="">시간 선택</option>
|
||||||
{times.map(t => <option key={t.id} value={t.id}>{t.startAt}</option>)}
|
{times.map(t => <option key={t.id} value={t.id}>{t.startAt}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>
|
<td>
|
||||||
<button className="btn btn-custom" onClick={handleSaveClick}>확인</button>
|
<button className="btn btn-primary" onClick={handleSaveClick}>확인</button>
|
||||||
<button className="btn btn-secondary" onClick={handleCancelClick}>취소</button>
|
<button className="btn btn-secondary" onClick={handleCancelClick}>취소</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -165,31 +167,33 @@ const AdminReservationPage: React.FC = () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className="filter-section ml-3">
|
</div>
|
||||||
|
<div className="filter-section section-card">
|
||||||
|
<h3 className="card-title">검색 필터</h3>
|
||||||
<form id="filter-form" onSubmit={applyFilter}>
|
<form id="filter-form" onSubmit={applyFilter}>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="member">예약자</label>
|
<label className="form-label" htmlFor="member">예약자</label>
|
||||||
<select id="member" name="memberId" className="form-control" onChange={handleFilterChange}>
|
<select id="member" name="memberId" className="form-select" onChange={handleFilterChange}>
|
||||||
<option value="">전체</option>
|
<option value="">전체</option>
|
||||||
{members.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
|
{members.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="theme">테마</label>
|
<label className="form-label" htmlFor="theme">테마</label>
|
||||||
<select id="theme" name="themeId" className="form-control" onChange={handleFilterChange}>
|
<select id="theme" name="themeId" className="form-select" onChange={handleFilterChange}>
|
||||||
<option value="">전체</option>
|
<option value="">전체</option>
|
||||||
{themes.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
|
{themes.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="date-from">From</label>
|
<label className="form-label" htmlFor="date-from">From</label>
|
||||||
<input type="date" id="date-from" name="dateFrom" className="form-control" onChange={handleFilterChange} />
|
<input type="date" id="date-from" name="dateFrom" className="form-input" onChange={handleFilterChange} />
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="date-to">To</label>
|
<label className="form-label" htmlFor="date-to">To</label>
|
||||||
<input type="date" id="date-to" name="dateTo" className="form-control" onChange={handleFilterChange} />
|
<input type="date" id="date-to" name="dateTo" className="form-input" onChange={handleFilterChange} />
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" className="btn btn-primary float-end">적용</button>
|
<button type="submit" className="btn btn-primary">적용</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { createTheme, fetchThemes, delTheme } from '@_api/theme/themeAPI';
|
import { fetchAdminThemes } from '@_api/theme/themeAPI';
|
||||||
|
import type {AdminThemeSummaryRetrieveResponse} from '@_api/theme/themeTypes';
|
||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
import { isLoginRequiredError } from '@_api/apiClient';
|
||||||
|
import '../../css/admin-theme-page.css';
|
||||||
|
|
||||||
const AdminThemePage: React.FC = () => {
|
const AdminThemePage: React.FC = () => {
|
||||||
const [themes, setThemes] = useState<any[]>([]);
|
const [themes, setThemes] = useState<AdminThemeSummaryRetrieveResponse[]>([]);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
const [newTheme, setNewTheme] = useState({ name: '', description: '', thumbnail: '' });
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@ -23,88 +23,59 @@ const AdminThemePage: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
await fetchThemes()
|
try {
|
||||||
.then(response => setThemes(response.themes))
|
const response = await fetchAdminThemes();
|
||||||
.catch(handleError);
|
setThemes(response.themes);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAddClick = () => {
|
const handleAddClick = () => {
|
||||||
setIsEditing(true);
|
navigate('/admin/theme/edit/new');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelClick = () => {
|
const handleManageClick = (themeId: number) => {
|
||||||
setIsEditing(false);
|
navigate(`/admin/theme/edit/${themeId}`);
|
||||||
setNewTheme({ name: '', description: '', thumbnail: '' });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveClick = async () => {
|
|
||||||
await createTheme(newTheme)
|
|
||||||
.then((response) => {
|
|
||||||
setThemes([...themes, response]);
|
|
||||||
alert('테마를 추가했어요.');
|
|
||||||
handleCancelClick();
|
|
||||||
})
|
|
||||||
.catch(handleError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteTheme = async (id: string) => {
|
|
||||||
if (!window.confirm('정말 삭제하시겠어요?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await delTheme(id)
|
|
||||||
.then(() => {
|
|
||||||
setThemes(themes.filter(theme => theme.id !== id));
|
|
||||||
alert('테마를 삭제했어요.');
|
|
||||||
})
|
|
||||||
.catch(handleError);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content-container">
|
<div className="admin-theme-container">
|
||||||
<h2 className="content-container-title">테마 관리 페이지</h2>
|
<h2 className="page-title">테마 관리</h2>
|
||||||
|
<div className="section-card">
|
||||||
<div className="table-header">
|
<div className="table-header">
|
||||||
<button id="add-button" className="btn btn-custom mb-2 float-end" onClick={handleAddClick}>테마 추가</button>
|
<button className="btn btn-primary" onClick={handleAddClick}>테마 추가</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="table-container" />
|
<div className="table-container">
|
||||||
<table className="table">
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">순서</th>
|
<th>이름</th>
|
||||||
<th scope="col">제목</th>
|
<th>난이도</th>
|
||||||
<th scope="col">설명</th>
|
<th>가격</th>
|
||||||
<th scope="col">썸네일 URL</th>
|
<th>공개여부</th>
|
||||||
<th scope="col"></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="table-body">
|
<tbody>
|
||||||
{themes.map(theme => (
|
{themes.map(theme => (
|
||||||
<tr key={theme.id}>
|
<tr key={theme.id}>
|
||||||
<td>{theme.id}</td>
|
|
||||||
<td>{theme.name}</td>
|
<td>{theme.name}</td>
|
||||||
<td>{theme.description}</td>
|
<td>{theme.difficulty}</td>
|
||||||
<td>{theme.thumbnail}</td>
|
<td>{theme.price.toLocaleString()}원</td>
|
||||||
|
<td>{theme.isOpen ? '공개' : '비공개'}</td>
|
||||||
<td>
|
<td>
|
||||||
<button className="btn btn-danger" onClick={() => deleteTheme(theme.id)}>삭제</button>
|
<button className="btn btn-secondary" onClick={() => handleManageClick(theme.id)}>관리</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{isEditing && (
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td><input type="text" className="form-control" placeholder="이름" value={newTheme.name} onChange={e => setNewTheme({ ...newTheme, name: e.target.value })} /></td>
|
|
||||||
<td><input type="text" className="form-control" placeholder="설명" value={newTheme.description} onChange={e => setNewTheme({ ...newTheme, description: e.target.value })} /></td>
|
|
||||||
<td><input type="text" className="form-control" placeholder="썸네일 URL" value={newTheme.thumbnail} onChange={e => setNewTheme({ ...newTheme, thumbnail: e.target.value })} /></td>
|
|
||||||
<td>
|
|
||||||
<button className="btn btn-custom" onClick={handleSaveClick}>확인</button>
|
|
||||||
<button className="btn btn-secondary" onClick={handleCancelClick}>취소</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { TimeCreateRequest } from '@_api/time/timeTypes';
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
import { isLoginRequiredError } from '@_api/apiClient';
|
||||||
|
import '../../css/admin-time-page.css';
|
||||||
|
|
||||||
const AdminTimePage: React.FC = () => {
|
const AdminTimePage: React.FC = () => {
|
||||||
const [times, setTimes] = useState<any[]>([]);
|
const [times, setTimes] = useState<any[]>([]);
|
||||||
@ -76,21 +77,22 @@ const AdminTimePage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content-container">
|
<div className="admin-time-container">
|
||||||
<h2 className="content-container-title">시간 관리 페이지</h2>
|
<h2 className="page-title">시간 관리</h2>
|
||||||
|
<div className="section-card">
|
||||||
<div className="table-header">
|
<div className="table-header">
|
||||||
<button id="add-button" className="btn btn-custom mb-2 float-end" onClick={handleAddClick}>예약시간 추가</button>
|
<button className="btn btn-primary" onClick={handleAddClick}>시간 추가</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="table-container" />
|
<div className="table-container">
|
||||||
<table className="table">
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">순서</th>
|
<th>ID</th>
|
||||||
<th scope="col">시간</th>
|
<th>시간</th>
|
||||||
<th scope="col"></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="table-body">
|
<tbody>
|
||||||
{times.map(time => (
|
{times.map(time => (
|
||||||
<tr key={time.id}>
|
<tr key={time.id}>
|
||||||
<td>{time.id}</td>
|
<td>{time.id}</td>
|
||||||
@ -101,11 +103,11 @@ const AdminTimePage: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<tr>
|
<tr className="editing-row">
|
||||||
<td></td>
|
<td></td>
|
||||||
<td><input type="time" className="form-control" value={newTime} onChange={e => setNewTime(e.target.value)} /></td>
|
<td><input type="time" className="form-input" value={newTime} onChange={e => setNewTime(e.target.value)} /></td>
|
||||||
<td>
|
<td>
|
||||||
<button className="btn btn-custom" onClick={handleSaveClick}>확인</button>
|
<button className="btn btn-primary" onClick={handleSaveClick}>확인</button>
|
||||||
<button className="btn btn-secondary" onClick={handleCancelClick}>취소</button>
|
<button className="btn btn-secondary" onClick={handleCancelClick}>취소</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -113,6 +115,8 @@ const AdminTimePage: React.FC = () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { ReservationRetrieveResponse } from '@_api/reservation/reservationT
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
import { isLoginRequiredError } from '@_api/apiClient';
|
||||||
|
import '../../css/admin-waiting-page.css';
|
||||||
|
|
||||||
const AdminWaitingPage: React.FC = () => {
|
const AdminWaitingPage: React.FC = () => {
|
||||||
const [waitings, setWaitings] = useState<ReservationRetrieveResponse[]>([]);
|
const [waitings, setWaitings] = useState<ReservationRetrieveResponse[]>([]);
|
||||||
@ -48,21 +49,22 @@ const AdminWaitingPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content-container">
|
<div className="admin-waiting-container">
|
||||||
<h2 className="content-container-title">예약 대기 관리 페이지</h2>
|
<h2 className="page-title">예약 대기 관리</h2>
|
||||||
<div className="table-container" />
|
<div className="section-card">
|
||||||
<table className="table">
|
<div className="table-container">
|
||||||
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">예약대기 번호</th>
|
<th>예약대기 번호</th>
|
||||||
<th scope="col">예약자</th>
|
<th>예약자</th>
|
||||||
<th scope="col">테마</th>
|
<th>테마</th>
|
||||||
<th scope="col">날짜</th>
|
<th>날짜</th>
|
||||||
<th scope="col">시간</th>
|
<th>시간</th>
|
||||||
<th scope="col"></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="table-body">
|
<tbody>
|
||||||
{waitings.map(w => (
|
{waitings.map(w => (
|
||||||
<tr key={w.id}>
|
<tr key={w.id}>
|
||||||
<td>{w.id}</td>
|
<td>{w.id}</td>
|
||||||
@ -71,7 +73,7 @@ const AdminWaitingPage: React.FC = () => {
|
|||||||
<td>{w.date}</td>
|
<td>{w.date}</td>
|
||||||
<td>{w.time.startAt}</td>
|
<td>{w.time.startAt}</td>
|
||||||
<td>
|
<td>
|
||||||
<button className="btn btn-primary mr-2" onClick={() => approveWaiting(w.id)}>승인</button>
|
<button className="btn btn-primary" onClick={() => approveWaiting(w.id)}>승인</button>
|
||||||
<button className="btn btn-danger" onClick={() => denyWaiting(w.id)}>거절</button>
|
<button className="btn btn-danger" onClick={() => denyWaiting(w.id)}>거절</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -79,6 +81,8 @@ const AdminWaitingPage: React.FC = () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
39
frontend/src/pages/v2/HomePageV2.tsx
Normal file
39
frontend/src/pages/v2/HomePageV2.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { mostReservedThemes } from '../../api/theme/themeAPI';
|
||||||
|
import '../../css/home-page-v2.css';
|
||||||
|
|
||||||
|
const HomePageV2: React.FC = () => {
|
||||||
|
const [ranking, setRanking] = useState<any[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await mostReservedThemes(10);
|
||||||
|
setRanking(response.themes);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching ranking:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="home-container-v2">
|
||||||
|
<h2 className="page-title">인기 테마</h2>
|
||||||
|
<div className="theme-ranking-list-v2">
|
||||||
|
{ranking.map(theme => (
|
||||||
|
<div key={theme.id} className="theme-ranking-item-v2">
|
||||||
|
<img className="thumbnail" src={theme.thumbnail} alt={theme.name} />
|
||||||
|
<div className="theme-info">
|
||||||
|
<h5 className="theme-name">{theme.name}</h5>
|
||||||
|
<p className="theme-description">{theme.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomePageV2;
|
||||||
63
frontend/src/pages/v2/LoginPageV2.tsx
Normal file
63
frontend/src/pages/v2/LoginPageV2.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
|
import '../../css/login-page-v2.css';
|
||||||
|
|
||||||
|
const LoginPageV2: React.FC = () => {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const { login } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const from = location.state?.from?.pathname || '/';
|
||||||
|
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
await login({email, password});
|
||||||
|
|
||||||
|
alert('로그인에 성공했어요!');
|
||||||
|
navigate(from, { replace: true });
|
||||||
|
} catch (error: any) {
|
||||||
|
const message = error.response?.data?.message || '로그인에 실패했어요. 이메일과 비밀번호를 확인해주세요.';
|
||||||
|
alert(message);
|
||||||
|
console.error('로그인 실패:', error);
|
||||||
|
setPassword('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="login-container-v2">
|
||||||
|
<h2 className="page-title">로그인</h2>
|
||||||
|
<form className="login-form-v2" onSubmit={handleLogin}>
|
||||||
|
<div className="form-group">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="이메일"
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="비밀번호"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="button-group">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => navigate('/signup')}>회원가입</button>
|
||||||
|
<button type="submit" className="btn btn-primary">로그인</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPageV2;
|
||||||
@ -1,4 +1,7 @@
|
|||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
import { isLoginRequiredError } from '@_api/apiClient';
|
||||||
|
import { createPendingReservation } from '@_api/reservation/reservationAPI';
|
||||||
|
import { fetchUserThemes } from '@_api/theme/themeAPI';
|
||||||
|
import { fetchTimesWithAvailability } from '@_api/time/timeAPI';
|
||||||
import type { TimeWithAvailabilityResponse } from '@_api/time/timeTypes';
|
import type { TimeWithAvailabilityResponse } from '@_api/time/timeTypes';
|
||||||
import '@_css/reservation-v2-1.css';
|
import '@_css/reservation-v2-1.css';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
@ -7,7 +10,7 @@ import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
|
|||||||
|
|
||||||
// New theme type based on the provided schema
|
// New theme type based on the provided schema
|
||||||
interface ThemeV21 {
|
interface ThemeV21 {
|
||||||
id: string;
|
id: string; // Changed to number to match API
|
||||||
name: string;
|
name: string;
|
||||||
difficulty: string;
|
difficulty: string;
|
||||||
description: string;
|
description: string;
|
||||||
@ -44,83 +47,35 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mockThemes: ThemeV21[] = [
|
const fetchData = async () => {
|
||||||
{
|
try {
|
||||||
id: '1',
|
const response = await fetchUserThemes();
|
||||||
name: '우주 감옥 탈출',
|
// Map UserThemeRetrieveResponse to ThemeV21
|
||||||
difficulty: '어려움',
|
const mappedThemes: ThemeV21[] = response.themes.map(theme => ({
|
||||||
description: '당신은 우주에서 가장 악명 높은 감옥에 갇혔습니다. 동료들과 협력하여 감시 시스템을 뚫고 탈출하세요!',
|
id: theme.id,
|
||||||
thumbnailUrl: 'https://example.com/space-prison.jpg',
|
name: theme.name,
|
||||||
price: 28000,
|
difficulty: theme.difficulty,
|
||||||
minParticipants: 2,
|
description: theme.description,
|
||||||
maxParticipants: 5,
|
thumbnailUrl: theme.thumbnailUrl,
|
||||||
expectedMinutesFrom: 60,
|
price: theme.price,
|
||||||
expectedMinutesTo: 75,
|
minParticipants: theme.minParticipants,
|
||||||
availableMinutes: 90,
|
maxParticipants: theme.maxParticipants,
|
||||||
},
|
expectedMinutesFrom: theme.expectedMinutesFrom,
|
||||||
{
|
expectedMinutesTo: theme.expectedMinutesTo,
|
||||||
id: '2',
|
availableMinutes: theme.availableMinutes,
|
||||||
name: '마법사의 서재',
|
}));
|
||||||
difficulty: '보통',
|
setThemes(mappedThemes);
|
||||||
description: '전설적인 마법사의 비밀 서재에 들어왔습니다. 숨겨진 마법 주문을 찾아 세상을 구원하세요.',
|
} catch (error) {
|
||||||
thumbnailUrl: 'https://example.com/wizard-library.jpg',
|
handleError(error);
|
||||||
price: 25000,
|
}
|
||||||
minParticipants: 2,
|
|
||||||
maxParticipants: 4,
|
|
||||||
expectedMinutesFrom: 50,
|
|
||||||
expectedMinutesTo: 60,
|
|
||||||
availableMinutes: 70,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: '해적선 대탐험',
|
|
||||||
difficulty: '쉬움',
|
|
||||||
description: '전설의 해적선에 숨겨진 보물을 찾아 떠나는 모험! 가족, 친구와 함께 즐거운 시간을 보내세요.',
|
|
||||||
thumbnailUrl: 'https://example.com/pirate-ship.jpg',
|
|
||||||
price: 22000,
|
|
||||||
minParticipants: 3,
|
|
||||||
maxParticipants: 6,
|
|
||||||
expectedMinutesFrom: 45,
|
|
||||||
expectedMinutesTo: 55,
|
|
||||||
availableMinutes: 80,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const fetchMockThemes = () => {
|
|
||||||
return new Promise<{ themes: ThemeV21[] }>((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve({ themes: mockThemes });
|
|
||||||
}, 500); // 0.5초 딜레이
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
fetchData();
|
||||||
fetchMockThemes().then(res => setThemes(res.themes)).catch(handleError);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedDate && selectedTheme) {
|
if (selectedDate && selectedTheme) {
|
||||||
const mockTimes: TimeWithAvailabilityResponse[] = [
|
const dateStr = selectedDate.toLocaleDateString('en-CA');
|
||||||
{ id: 't1', startAt: '10:00', isAvailable: Math.random() > 0.3 },
|
fetchTimesWithAvailability(dateStr, selectedTheme.id)
|
||||||
{ id: 't2', startAt: '11:15', isAvailable: Math.random() > 0.3 },
|
|
||||||
{ id: 't3', startAt: '12:30', isAvailable: Math.random() > 0.3 },
|
|
||||||
{ id: 't4', startAt: '13:45', isAvailable: Math.random() > 0.3 },
|
|
||||||
{ id: 't5', startAt: '15:00', isAvailable: Math.random() > 0.3 },
|
|
||||||
{ id: 't6', startAt: '16:15', isAvailable: Math.random() > 0.3 },
|
|
||||||
{ id: 't7', startAt: '17:30', isAvailable: Math.random() > 0.3 },
|
|
||||||
{ id: 't8', startAt: '18:45', isAvailable: Math.random() > 0.3 },
|
|
||||||
{ id: 't9', startAt: '20:00', isAvailable: Math.random() > 0.3 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const fetchMockTimes = (date: Date, themeId: string) => {
|
|
||||||
console.log(`Fetching mock times for ${date.toLocaleDateString()} and theme ${themeId}`);
|
|
||||||
return new Promise<{ times: TimeWithAvailabilityResponse[] }>((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve({ times: mockTimes });
|
|
||||||
}, 300); // 0.3초 딜레이
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchMockTimes(selectedDate, selectedTheme.id)
|
|
||||||
.then(res => {
|
.then(res => {
|
||||||
setTimes(res.times);
|
setTimes(res.times);
|
||||||
setSelectedTime(null);
|
setSelectedTime(null);
|
||||||
@ -150,28 +105,13 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
timeId: selectedTime.id,
|
timeId: selectedTime.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock createPendingReservation to include price
|
createPendingReservation(reservationData)
|
||||||
const mockCreatePendingReservation = (data: typeof reservationData) => {
|
|
||||||
console.log("Creating pending reservation with:", data);
|
|
||||||
return new Promise<any>((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve({
|
|
||||||
reservationId: `res-${crypto.randomUUID()}`,
|
|
||||||
themeName: selectedTheme?.name,
|
|
||||||
date: data.date,
|
|
||||||
startAt: selectedTime?.startAt,
|
|
||||||
price: selectedTheme?.price, // Include the price in the response
|
|
||||||
});
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
mockCreatePendingReservation(reservationData)
|
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
navigate('/v2-1/reservation/payment', { state: { reservation: res } });
|
navigate('/v2/reservation/payment', { state: { reservation: res } });
|
||||||
})
|
})
|
||||||
.catch(handleError)
|
.catch(handleError)
|
||||||
.finally(() => setIsConfirmModalOpen(false));
|
.finally(() => setIsModalOpen(false));
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderDateOptions = () => {
|
const renderDateOptions = () => {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
import { confirmReservationPayment } from '@_api/reservation/reservationAPI';
|
|
||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
import { isLoginRequiredError } from '@_api/apiClient';
|
||||||
|
import { confirmReservationPayment } from '@_api/reservation/reservationAPI';
|
||||||
import { PaymentType, type ReservationCreateResponse, type ReservationPaymentRequest } from '@_api/reservation/reservationTypes';
|
import { PaymentType, type ReservationCreateResponse, type ReservationPaymentRequest } from '@_api/reservation/reservationTypes';
|
||||||
import '@_css/reservation-v2-1.css'; // Reuse the new CSS for consistency
|
import '@_css/reservation-v2-1.css'; // Reuse the new CSS for consistency
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
|
import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@ -21,7 +21,7 @@ const ReservationStep2PageV21: React.FC = () => {
|
|||||||
|
|
||||||
// The reservation object now contains the price
|
// The reservation object now contains the price
|
||||||
const reservation: ReservationCreateResponse & { price: number } | undefined = location.state?.reservation;
|
const reservation: ReservationCreateResponse & { price: number } | undefined = location.state?.reservation;
|
||||||
|
console.log(reservation)
|
||||||
const handleError = (err: any) => {
|
const handleError = (err: any) => {
|
||||||
if (isLoginRequiredError(err)) {
|
if (isLoginRequiredError(err)) {
|
||||||
alert('로그인이 필요해요.');
|
alert('로그인이 필요해요.');
|
||||||
@ -71,7 +71,7 @@ const ReservationStep2PageV21: React.FC = () => {
|
|||||||
paymentWidgetRef.current.requestPayment({
|
paymentWidgetRef.current.requestPayment({
|
||||||
orderId: generateRandomString(),
|
orderId: generateRandomString(),
|
||||||
orderName: `${reservation.themeName} 예약 결제`,
|
orderName: `${reservation.themeName} 예약 결제`,
|
||||||
amount: reservation.price, // Use the price here as well
|
amount: reservation.price,
|
||||||
}).then((data: any) => {
|
}).then((data: any) => {
|
||||||
const paymentData: ReservationPaymentRequest = {
|
const paymentData: ReservationPaymentRequest = {
|
||||||
paymentKey: data.paymentKey,
|
paymentKey: data.paymentKey,
|
||||||
|
|||||||
70
frontend/src/pages/v2/SignupPageV2.tsx
Normal file
70
frontend/src/pages/v2/SignupPageV2.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { signup } from '../../api/member/memberAPI';
|
||||||
|
import type { SignupRequest } from '../../api/member/memberTypes';
|
||||||
|
import '../../css/signup-page-v2.css';
|
||||||
|
|
||||||
|
const SignupPageV2: React.FC = () => {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleSignup = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const request: SignupRequest = { email, password, name };
|
||||||
|
try {
|
||||||
|
const response = await signup(request);
|
||||||
|
alert(`${response.name}님, 회원가입을 축하드려요. 로그인 후 이용해주세요!`);
|
||||||
|
navigate('/v2/login');
|
||||||
|
} catch (error: any) {
|
||||||
|
const message = error.response?.data?.message || '회원가입에 실패했어요. 입력 정보를 확인해주세요.';
|
||||||
|
alert(message);
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="signup-container-v2">
|
||||||
|
<h2 className="page-title">회원가입</h2>
|
||||||
|
<form className="signup-form-v2" onSubmit={handleSignup}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">이메일</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="이메일을 입력하세요"
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">비밀번호</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="비밀번호를 입력하세요"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">이름</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="이름을 입력하세요"
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" className="btn-primary">가입하기</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignupPageV2;
|
||||||
55
src/main/resources/login.http
Normal file
55
src/main/resources/login.http
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
POST http://localhost:8080/login?key=value&key1=value1
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "a@a.a",
|
||||||
|
"password": "a"
|
||||||
|
}
|
||||||
|
|
||||||
|
> {%
|
||||||
|
const accessToken = response.body.data.accessToken;
|
||||||
|
client.global.set("token", accessToken);
|
||||||
|
%}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET http://localhost:8080/reservations
|
||||||
|
Accept: application/json
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
DELETE http://localhost:8080/reservations/57
|
||||||
|
Accept: application/json
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
POST http://localhost:8080/reservations/admin
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"date": "2026-10-01",
|
||||||
|
"timeId": 1,
|
||||||
|
"themeId": 1,
|
||||||
|
"memberId": 3
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
POST http://localhost:8080/reservations/admin
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"date": "2023-10-01",
|
||||||
|
"timeId": 1,
|
||||||
|
"themeId": 1,
|
||||||
|
"memberId": 3
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET http://localhost:8080/reservations-mine
|
||||||
|
Accept: application/json
|
||||||
Loading…
x
Reference in New Issue
Block a user