generated from pricelees/issue-pr-template
[#41] 예약 스키마 재정의 #42
@ -23,6 +23,7 @@ import ReservationSuccessPageV21 from './pages/v2/ReservationSuccessPageV21';
|
|||||||
import HomePageV2 from './pages/v2/HomePageV2';
|
import HomePageV2 from './pages/v2/HomePageV2';
|
||||||
import LoginPageV2 from './pages/v2/LoginPageV2';
|
import LoginPageV2 from './pages/v2/LoginPageV2';
|
||||||
import SignupPageV2 from './pages/v2/SignupPageV2';
|
import SignupPageV2 from './pages/v2/SignupPageV2';
|
||||||
|
import ReservationFormPage from './pages/v2/ReservationFormPage';
|
||||||
import AdminThemeEditPage from './pages/admin/AdminThemeEditPage';
|
import AdminThemeEditPage from './pages/admin/AdminThemeEditPage';
|
||||||
import AdminSchedulePage from './pages/admin/AdminSchedulePage';
|
import AdminSchedulePage from './pages/admin/AdminSchedulePage';
|
||||||
|
|
||||||
@ -72,6 +73,7 @@ function App() {
|
|||||||
|
|
||||||
{/* V2.1 Reservation Flow */}
|
{/* V2.1 Reservation Flow */}
|
||||||
<Route path="/v2-1/reservation" element={<ReservationStep1PageV21 />} />
|
<Route path="/v2-1/reservation" element={<ReservationStep1PageV21 />} />
|
||||||
|
<Route path="/v2/reservation/form" element={<ReservationFormPage />} />
|
||||||
<Route path="/v2-1/reservation/payment" element={<ReservationStep2PageV21 />} />
|
<Route path="/v2-1/reservation/payment" element={<ReservationStep2PageV21 />} />
|
||||||
<Route path="/v2-1/reservation/success" element={<ReservationSuccessPageV21 />} />
|
<Route path="/v2-1/reservation/success" element={<ReservationSuccessPageV21 />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@ -17,3 +17,9 @@ export interface SignupResponse {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MemberSummaryRetrieveResponse {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|||||||
73
frontend/src/api/payment/PaymentTypes.ts
Normal file
73
frontend/src/api/payment/PaymentTypes.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
export interface PaymentConfirmRequest {
|
||||||
|
paymentKey: string;
|
||||||
|
orderId: string;
|
||||||
|
amount: number;
|
||||||
|
paymentType: PaymentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentCancelRequest {
|
||||||
|
reservationId: string,
|
||||||
|
cancelReason: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// V2 types
|
||||||
|
export const PaymentType = {
|
||||||
|
NORMAL: 'NORMAL',
|
||||||
|
BILLING: 'BILLING',
|
||||||
|
BRANDPAY: 'BRANDPAY'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type PaymentType =
|
||||||
|
| typeof PaymentType.NORMAL
|
||||||
|
| typeof PaymentType.BILLING
|
||||||
|
| typeof PaymentType.BRANDPAY;
|
||||||
|
|
||||||
|
export interface PaymentCreateResponseV2 {
|
||||||
|
paymentId: string;
|
||||||
|
detailId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentRetrieveResponse {
|
||||||
|
orderId: string;
|
||||||
|
totalAmount: number;
|
||||||
|
method: string;
|
||||||
|
status: 'DONE' | 'CANCELED';
|
||||||
|
requestedAt: string;
|
||||||
|
approvedAt: string;
|
||||||
|
detail: CardPaymentDetail | BankTransferPaymentDetail | EasyPayPrepaidPaymentDetail;
|
||||||
|
cancellation?: CanceledPaymentDetailResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardPaymentDetail {
|
||||||
|
type: 'CARD';
|
||||||
|
issuerCode: string;
|
||||||
|
cardType: 'CREDIT' | 'CHECK' | 'GIFT';
|
||||||
|
ownerType: 'PERSONAL' | 'CORPORATE';
|
||||||
|
cardNumber: string;
|
||||||
|
amount: number;
|
||||||
|
approvalNumber: string;
|
||||||
|
installmentPlanMonths: number;
|
||||||
|
isInterestFree: boolean;
|
||||||
|
easypayProviderName?: string;
|
||||||
|
easypayDiscountAmount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BankTransferPaymentDetail {
|
||||||
|
type: 'BANK_TRANSFER';
|
||||||
|
bankName: string;
|
||||||
|
settlementStatus: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EasyPayPrepaidPaymentDetail {
|
||||||
|
type: 'EASYPAY_PREPAID';
|
||||||
|
providerName: string;
|
||||||
|
amount: number;
|
||||||
|
discountAmount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CanceledPaymentDetailResponse {
|
||||||
|
cancellationRequestedAt: string; // ISO 8601 format
|
||||||
|
cancellationApprovedAt: string; // ISO 8601 format
|
||||||
|
cancelReason: string;
|
||||||
|
canceledBy: string;
|
||||||
|
}
|
||||||
10
frontend/src/api/payment/paymentAPI.ts
Normal file
10
frontend/src/api/payment/paymentAPI.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import apiClient from "@_api/apiClient";
|
||||||
|
import type { PaymentCancelRequest, PaymentConfirmRequest, PaymentCreateResponseV2 } from "./PaymentTypes";
|
||||||
|
|
||||||
|
export const confirmPayment = async (reservationId: string, request: PaymentConfirmRequest): Promise<PaymentCreateResponseV2> => {
|
||||||
|
return await apiClient.post<PaymentCreateResponseV2>(`/payments?reservationId=${reservationId}`, request);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cancelPayment = async (request: PaymentCancelRequest): Promise<void> => {
|
||||||
|
return await apiClient.post(`/payments/cancel`, request);
|
||||||
|
};
|
||||||
@ -85,10 +85,7 @@ export const confirmReservationPayment = async (id: string, data: ReservationPay
|
|||||||
return await apiClient.post<ReservationPaymentResponse>(`/v2/reservations/${id}/pay`, data, true);
|
return await apiClient.post<ReservationPaymentResponse>(`/v2/reservations/${id}/pay`, data, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// POST /v2/reservations/{id}/cancel
|
|
||||||
export const cancelReservationV2 = async (id: string, cancelReason: string): Promise<void> => {
|
|
||||||
return await apiClient.post(`/v2/reservations/${id}/cancel`, { cancelReason }, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// GET /v2/reservations
|
// GET /v2/reservations
|
||||||
export const fetchMyReservationsV2 = async (): Promise<ReservationSummaryListV2> => {
|
export const fetchMyReservationsV2 = async (): Promise<ReservationSummaryListV2> => {
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
import apiClient from '../apiClient';
|
||||||
|
import type { PendingReservationCreateRequest, PendingReservationCreateResponse, ReservationDetailRetrieveResponse, ReservationSummaryRetrieveListResponse } from './reservationTypesV2';
|
||||||
|
|
||||||
|
export const createPendingReservation = async (request: PendingReservationCreateRequest): Promise<PendingReservationCreateResponse> => {
|
||||||
|
return await apiClient.post<PendingReservationCreateResponse>('/reservations/pending', request);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const confirmReservation = async (reservationId: string): Promise<void> => {
|
||||||
|
await apiClient.patch(`/reservations/${reservationId}/confirm`, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const cancelReservation = async (id: string, cancelReason: string): Promise<void> => {
|
||||||
|
return await apiClient.post(`/v3/reservations/${id}/cancel`, { cancelReason }, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchSummaryByMember = async (): Promise<ReservationSummaryRetrieveListResponse> => {
|
||||||
|
return await apiClient.get<ReservationSummaryRetrieveListResponse>('/v2/reservations/summary');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchDetailById = async (reservationId: string): Promise<ReservationDetailRetrieveResponse> => {
|
||||||
|
return await apiClient.get<ReservationDetailRetrieveResponse>(`/v2/reservations/${reservationId}/detail`);
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import type { MemberRetrieveResponse } from '@_api/member/memberTypes';
|
import type { MemberRetrieveResponse, MemberSummaryRetrieveResponse } from '@_api/member/memberTypes';
|
||||||
|
import type { PaymentRetrieveResponse, PaymentType } from '@_api/payment/PaymentTypes';
|
||||||
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';
|
||||||
|
|
||||||
@ -77,18 +78,6 @@ export interface ReservationSearchQuery {
|
|||||||
dateTo?: string;
|
dateTo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// V2 types
|
|
||||||
export const PaymentType = {
|
|
||||||
NORMAL: 'NORMAL',
|
|
||||||
BILLING: 'BILLING',
|
|
||||||
BRANDPAY: 'BRANDPAY'
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type PaymentType =
|
|
||||||
| typeof PaymentType.NORMAL
|
|
||||||
| typeof PaymentType.BILLING
|
|
||||||
| typeof PaymentType.BRANDPAY;
|
|
||||||
|
|
||||||
export const PaymentStatus = {
|
export const PaymentStatus = {
|
||||||
IN_PROGRESS: '결제 진행 중',
|
IN_PROGRESS: '결제 진행 중',
|
||||||
DONE: '결제 완료',
|
DONE: '결제 완료',
|
||||||
@ -123,7 +112,7 @@ export interface ReservationPaymentRequest {
|
|||||||
paymentKey: string;
|
paymentKey: string;
|
||||||
orderId: string;
|
orderId: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
paymentType: PaymentType
|
paymentType: PaymentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReservationPaymentResponse {
|
export interface ReservationPaymentResponse {
|
||||||
@ -133,75 +122,14 @@ export interface ReservationPaymentResponse {
|
|||||||
paymentStatus: PaymentStatus;
|
paymentStatus: PaymentStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReservationSummaryV2 {
|
|
||||||
id: string;
|
|
||||||
themeName: string;
|
|
||||||
date: string;
|
|
||||||
startAt: string;
|
|
||||||
status: string; // 'CONFIRMED', 'CANCELED_BY_USER', etc.
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReservationSummaryListV2 {
|
|
||||||
reservations: ReservationSummaryV2[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReservationDetailV2 {
|
export interface ReservationDetailV2 {
|
||||||
id: string;
|
id: string;
|
||||||
user: UserDetailV2;
|
user: MemberSummaryRetrieveResponse;
|
||||||
themeName: string;
|
themeName: string;
|
||||||
date: string;
|
date: string;
|
||||||
startAt: string;
|
startAt: string;
|
||||||
applicationDateTime: string;
|
applicationDateTime: string;
|
||||||
payment: PaymentV2;
|
payment: PaymentRetrieveResponse;
|
||||||
cancellation: CancellationV2 | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserDetailV2 {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaymentV2 {
|
|
||||||
orderId: string;
|
|
||||||
totalAmount: number;
|
|
||||||
method: string;
|
|
||||||
status: 'DONE' | 'CANCELED';
|
|
||||||
requestedAt: string;
|
|
||||||
approvedAt: string;
|
|
||||||
detail: CardPaymentDetailV2 | BankTransferPaymentDetailV2 | EasyPayPrepaidPaymentDetailV2;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardPaymentDetailV2 {
|
|
||||||
type: 'CARD';
|
|
||||||
issuerCode: string;
|
|
||||||
cardType: 'CREDIT' | 'CHECK' | 'GIFT';
|
|
||||||
ownerType: 'PERSONAL' | 'CORPORATE';
|
|
||||||
cardNumber: string;
|
|
||||||
amount: number;
|
|
||||||
approvalNumber: string;
|
|
||||||
installmentPlanMonths: number;
|
|
||||||
isInterestFree: boolean;
|
|
||||||
easypayProviderName?: string;
|
|
||||||
easypayDiscountAmount?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BankTransferPaymentDetailV2 {
|
|
||||||
type: 'BANK_TRANSFER';
|
|
||||||
bankName: string;
|
|
||||||
settlementStatus: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EasyPayPrepaidPaymentDetailV2 {
|
|
||||||
type: 'EASYPAY_PREPAID';
|
|
||||||
providerName: string;
|
|
||||||
amount: number;
|
|
||||||
discountAmount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CancellationV2 {
|
|
||||||
cancellationRequestedAt: string; // ISO 8601 format
|
|
||||||
cancellationApprovedAt: string; // ISO 8601 format
|
|
||||||
cancelReason: string;
|
|
||||||
canceledBy: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
58
frontend/src/api/reservation/reservationTypesV2.ts
Normal file
58
frontend/src/api/reservation/reservationTypesV2.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import type { MemberSummaryRetrieveResponse } from "@_api/member/memberTypes";
|
||||||
|
import type { PaymentRetrieveResponse } from "@_api/payment/PaymentTypes";
|
||||||
|
|
||||||
|
export const ReservationStatusV2 = {
|
||||||
|
PENDING: 'PENDING',
|
||||||
|
CONFIRMED: 'CONFIRMED',
|
||||||
|
CANCELED: 'CANCELED',
|
||||||
|
FAILED: 'FAILED',
|
||||||
|
EXPIRED: 'EXPIRED'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ReservationStatusV2 =
|
||||||
|
| typeof ReservationStatusV2.PENDING
|
||||||
|
| typeof ReservationStatusV2.CONFIRMED
|
||||||
|
| typeof ReservationStatusV2.CANCELED
|
||||||
|
| typeof ReservationStatusV2.FAILED
|
||||||
|
| typeof ReservationStatusV2.EXPIRED;
|
||||||
|
|
||||||
|
export interface PendingReservationCreateRequest {
|
||||||
|
scheduleId: string,
|
||||||
|
reserverName: string,
|
||||||
|
reserverContact: string,
|
||||||
|
participantCount: number,
|
||||||
|
requirement: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PendingReservationCreateResponse {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReservationSummaryRetrieveResponse {
|
||||||
|
id: string;
|
||||||
|
themeName: string;
|
||||||
|
date: string;
|
||||||
|
startAt: string;
|
||||||
|
status: ReservationStatusV2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReservationSummaryRetrieveListResponse {
|
||||||
|
reservations: ReservationSummaryRetrieveResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReservationDetailRetrieveResponse {
|
||||||
|
id: string;
|
||||||
|
member: MemberSummaryRetrieveResponse;
|
||||||
|
applicationDateTime: string;
|
||||||
|
payment: PaymentRetrieveResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReservationDetail {
|
||||||
|
id: string;
|
||||||
|
themeName: string;
|
||||||
|
date: string;
|
||||||
|
startAt: string;
|
||||||
|
member: MemberSummaryRetrieveResponse;
|
||||||
|
applicationDateTime: string;
|
||||||
|
payment: PaymentRetrieveResponse;
|
||||||
|
}
|
||||||
@ -30,3 +30,7 @@ export const updateSchedule = async (id: string, request: ScheduleUpdateRequest)
|
|||||||
export const deleteSchedule = async (id: string): Promise<void> => {
|
export const deleteSchedule = async (id: string): Promise<void> => {
|
||||||
await apiClient.del(`/schedules/${id}`);
|
await apiClient.del(`/schedules/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const holdSchedule = async (id: string): Promise<void> => {
|
||||||
|
await apiClient.patch(`/schedules/${id}/hold`, {});
|
||||||
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
export enum ScheduleStatus {
|
export enum ScheduleStatus {
|
||||||
AVAILABLE = 'AVAILABLE',
|
AVAILABLE = 'AVAILABLE',
|
||||||
PENDING = 'PENDING',
|
HOLD = 'HOLD',
|
||||||
RESERVED = 'RESERVED',
|
RESERVED = 'RESERVED',
|
||||||
BLOCKED = 'BLOCKED',
|
BLOCKED = 'BLOCKED',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -417,4 +417,81 @@
|
|||||||
}
|
}
|
||||||
.modal-actions .confirm-button:hover {
|
.modal-actions .confirm-button:hover {
|
||||||
background-color: #1B64DA;
|
background-color: #1B64DA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Styles for ReservationFormPage */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"],
|
||||||
|
.form-group input[type="tel"],
|
||||||
|
.form-group input[type="number"],
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus, .form-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3182F6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(49, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-control input {
|
||||||
|
text-align: center;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
width: 60px;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-control button {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-control button:hover:not(:disabled) {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-control button:disabled {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-control button:first-of-type {
|
||||||
|
border-radius: 8px 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-control button:last-of-type {
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ const getScheduleStatusText = (status: ScheduleStatus): string => {
|
|||||||
switch (status) {
|
switch (status) {
|
||||||
case ScheduleStatus.AVAILABLE:
|
case ScheduleStatus.AVAILABLE:
|
||||||
return '예약 가능';
|
return '예약 가능';
|
||||||
case ScheduleStatus.PENDING:
|
case ScheduleStatus.HOLD:
|
||||||
return '예약 진행 중';
|
return '예약 진행 중';
|
||||||
case ScheduleStatus.RESERVED:
|
case ScheduleStatus.RESERVED:
|
||||||
return '예약 완료';
|
return '예약 완료';
|
||||||
|
|||||||
@ -193,7 +193,7 @@ const AdminThemeEditPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label" htmlFor="price">가격 (원)</label>
|
<label className="form-label" htmlFor="price">1인당 요금 (원)</label>
|
||||||
<input id="price" name="price" type="number" className="form-input" value={theme.price} onChange={handleChange} required disabled={!isEditing} />
|
<input id="price" name="price" type="number" className="form-input" value={theme.price} onChange={handleChange} required disabled={!isEditing} />
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
|
|||||||
@ -37,7 +37,7 @@ const AdminThemePage: React.FC = () => {
|
|||||||
navigate('/admin/theme/edit/new');
|
navigate('/admin/theme/edit/new');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleManageClick = (themeId: number) => {
|
const handleManageClick = (themeId: string) => {
|
||||||
navigate(`/admin/theme/edit/${themeId}`);
|
navigate(`/admin/theme/edit/${themeId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ const AdminThemePage: React.FC = () => {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>이름</th>
|
<th>이름</th>
|
||||||
<th>난이도</th>
|
<th>난이도</th>
|
||||||
<th>가격</th>
|
<th>1인당 요금</th>
|
||||||
<th>공개여부</th>
|
<th>공개여부</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
|
import { cancelPayment } from '@_api/payment/paymentAPI';
|
||||||
|
import type { PaymentRetrieveResponse } from '@_api/payment/PaymentTypes';
|
||||||
|
import { cancelReservation, fetchDetailById, fetchSummaryByMember } from '@_api/reservation/reservationAPIV2';
|
||||||
|
import type { ReservationDetail, ReservationSummaryRetrieveResponse } from '@_api/reservation/reservationTypesV2';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
|
||||||
cancelReservationV2,
|
|
||||||
fetchMyReservationsV2,
|
|
||||||
fetchReservationDetailV2
|
|
||||||
} from '../../api/reservation/reservationAPI';
|
|
||||||
import type { PaymentV2, ReservationDetailV2, ReservationSummaryV2 } from '../../api/reservation/reservationTypes';
|
|
||||||
import '../../css/my-reservation-v2.css';
|
import '../../css/my-reservation-v2.css';
|
||||||
|
|
||||||
const formatDisplayDateTime = (dateTime: any): string => {
|
const formatDisplayDateTime = (dateTime: any): string => {
|
||||||
@ -78,7 +76,7 @@ const formatCardDateTime = (dateStr: string, timeStr: string): string => {
|
|||||||
|
|
||||||
// --- Cancellation View Component ---
|
// --- Cancellation View Component ---
|
||||||
const CancellationView: React.FC<{
|
const CancellationView: React.FC<{
|
||||||
reservation: ReservationDetailV2;
|
reservation: ReservationDetail;
|
||||||
onCancelSubmit: (reason: string) => void;
|
onCancelSubmit: (reason: string) => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
isCancelling: boolean;
|
isCancelling: boolean;
|
||||||
@ -119,13 +117,12 @@ const CancellationView: React.FC<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// --- Reservation Detail View Component ---
|
|
||||||
const ReservationDetailView: React.FC<{
|
const ReservationDetailView: React.FC<{
|
||||||
reservation: ReservationDetailV2;
|
reservation: ReservationDetail;
|
||||||
onGoToCancel: () => void;
|
onGoToCancel: () => void;
|
||||||
}> = ({ reservation, onGoToCancel }) => {
|
}> = ({ reservation, onGoToCancel }) => {
|
||||||
|
|
||||||
const renderPaymentDetails = (payment: PaymentV2) => {
|
const renderPaymentDetails = (payment: PaymentRetrieveResponse) => {
|
||||||
const { detail } = payment;
|
const { detail } = payment;
|
||||||
|
|
||||||
switch (detail.type) {
|
switch (detail.type) {
|
||||||
@ -178,8 +175,8 @@ const ReservationDetailView: React.FC<{
|
|||||||
<h3>예약 정보</h3>
|
<h3>예약 정보</h3>
|
||||||
<p><strong>예약 테마:</strong> {reservation.themeName}</p>
|
<p><strong>예약 테마:</strong> {reservation.themeName}</p>
|
||||||
<p><strong>이용 예정일:</strong> {formatCardDateTime(reservation.date, reservation.startAt)}</p>
|
<p><strong>이용 예정일:</strong> {formatCardDateTime(reservation.date, reservation.startAt)}</p>
|
||||||
<p><strong>예약자 이름:</strong> {reservation.user.name}</p>
|
<p><strong>예약자 이름:</strong> {reservation.member.name}</p>
|
||||||
<p><strong>예약자 이메일:</strong> {reservation.user.email}</p>
|
<p><strong>예약자 이메일:</strong> {reservation.member.email}</p>
|
||||||
<p><strong>예약 신청 일시:</strong> {formatDisplayDateTime(reservation.applicationDateTime)}</p>
|
<p><strong>예약 신청 일시:</strong> {formatDisplayDateTime(reservation.applicationDateTime)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-section-v2">
|
<div className="modal-section-v2">
|
||||||
@ -188,13 +185,13 @@ const ReservationDetailView: React.FC<{
|
|||||||
{renderPaymentDetails(reservation.payment)}
|
{renderPaymentDetails(reservation.payment)}
|
||||||
<p><strong>결제 승인 일시:</strong> {formatDisplayDateTime(reservation.payment.approvedAt)}</p>
|
<p><strong>결제 승인 일시:</strong> {formatDisplayDateTime(reservation.payment.approvedAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
{reservation.cancellation && (
|
{reservation.payment.cancellation && (
|
||||||
<div className="modal-section-v2 cancellation-section-v2">
|
<div className="modal-section-v2 cancellation-section-v2">
|
||||||
<h3>취소 정보</h3>
|
<h3>취소 정보</h3>
|
||||||
<p><strong>취소 요청 일시:</strong> {formatDisplayDateTime(reservation.cancellation.cancellationRequestedAt)}</p>
|
<p><strong>취소 요청 일시:</strong> {formatDisplayDateTime(reservation.payment.cancellation.cancellationRequestedAt)}</p>
|
||||||
<p><strong>환불 완료 일시:</strong> {formatDisplayDateTime(reservation.cancellation.cancellationApprovedAt)}</p>
|
<p><strong>환불 완료 일시:</strong> {formatDisplayDateTime(reservation.payment.cancellation.cancellationApprovedAt)}</p>
|
||||||
<p><strong>취소 사유:</strong> {reservation.cancellation.cancelReason}</p>
|
<p><strong>취소 사유:</strong> {reservation.payment.cancellation.cancelReason}</p>
|
||||||
<p><strong>취소 요청자:</strong> {reservation.cancellation.canceledBy == reservation.user.id ? '회원 본인' : '관리자'}</p>
|
<p><strong>취소 요청자:</strong> {reservation.payment.cancellation.canceledBy == reservation.member.id ? '회원 본인' : '관리자'}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{reservation.payment.status !== 'CANCELED' && (
|
{reservation.payment.status !== 'CANCELED' && (
|
||||||
@ -208,11 +205,11 @@ const ReservationDetailView: React.FC<{
|
|||||||
|
|
||||||
// --- Main Page Component ---
|
// --- Main Page Component ---
|
||||||
const MyReservationPageV2: React.FC = () => {
|
const MyReservationPageV2: React.FC = () => {
|
||||||
const [reservations, setReservations] = useState<ReservationSummaryV2[]>([]);
|
const [reservations, setReservations] = useState<ReservationSummaryRetrieveResponse[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const [selectedReservation, setSelectedReservation] = useState<ReservationDetailV2 | null>(null);
|
const [selectedReservation, setSelectedReservation] = useState<ReservationDetail | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [isDetailLoading, setIsDetailLoading] = useState(false);
|
const [isDetailLoading, setIsDetailLoading] = useState(false);
|
||||||
const [detailError, setDetailError] = useState<string | null>(null);
|
const [detailError, setDetailError] = useState<string | null>(null);
|
||||||
@ -223,7 +220,7 @@ const MyReservationPageV2: React.FC = () => {
|
|||||||
const loadReservations = async () => {
|
const loadReservations = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const data = await fetchMyReservationsV2();
|
const data = await fetchSummaryByMember();
|
||||||
setReservations(data.reservations);
|
setReservations(data.reservations);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -237,14 +234,21 @@ const MyReservationPageV2: React.FC = () => {
|
|||||||
loadReservations();
|
loadReservations();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleShowDetail = async (id: string) => {
|
const handleShowDetail = async (id: string, themeName: string, date: string, time: string) => {
|
||||||
try {
|
try {
|
||||||
setIsDetailLoading(true);
|
setIsDetailLoading(true);
|
||||||
setDetailError(null);
|
setDetailError(null);
|
||||||
setModalView('detail');
|
setModalView('detail');
|
||||||
const detailData = await fetchReservationDetailV2(id);
|
const detailData = await fetchDetailById(id);
|
||||||
console.log('상세 정보:', detailData);
|
setSelectedReservation({
|
||||||
setSelectedReservation(detailData);
|
id: detailData.id,
|
||||||
|
themeName: themeName,
|
||||||
|
date: date,
|
||||||
|
startAt: time,
|
||||||
|
member: detailData.member,
|
||||||
|
applicationDateTime: detailData.applicationDateTime,
|
||||||
|
payment: detailData.payment
|
||||||
|
});
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setDetailError('예약 상세 정보를 불러오는 데 실패했습니다.');
|
setDetailError('예약 상세 정보를 불러오는 데 실패했습니다.');
|
||||||
@ -268,16 +272,18 @@ const MyReservationPageV2: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
setIsCancelling(true);
|
setIsCancelling(true);
|
||||||
setDetailError(null);
|
setDetailError(null);
|
||||||
await cancelReservationV2(selectedReservation.id, reason);
|
await cancelReservation(selectedReservation.id, reason);
|
||||||
|
cancelPayment({ reservationId: selectedReservation.id, cancelReason: reason });
|
||||||
alert('예약을 취소했어요. 결제 취소까지는 3-5일 정도 소요될 수 있어요.');
|
alert('예약을 취소했어요. 결제 취소까지는 3-5일 정도 소요될 수 있어요.');
|
||||||
handleCloseModal();
|
handleCloseModal();
|
||||||
loadReservations(); // Refresh the list
|
await loadReservations(); // Refresh the list
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setDetailError(err instanceof Error ? err.message : '예약 취소에 실패했습니다.');
|
setDetailError(err instanceof Error ? err.message : '예약 취소에 실패했습니다.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsCancelling(false);
|
setIsCancelling(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
console.log("reservations=", reservations);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-reservation-container-v2">
|
<div className="my-reservation-container-v2">
|
||||||
@ -289,14 +295,13 @@ const MyReservationPageV2: React.FC = () => {
|
|||||||
{!isLoading && !error && (
|
{!isLoading && !error && (
|
||||||
<div className="reservation-list-v2">
|
<div className="reservation-list-v2">
|
||||||
{reservations.map((res) => (
|
{reservations.map((res) => (
|
||||||
console.log(res),
|
<div key={res.id} className={`reservation-summary-card-v2 status-${res.status.toString().toLowerCase()}`}>
|
||||||
<div key={res.id} className={`reservation-summary-card-v2 status-${res.status.toLowerCase()}`}>
|
|
||||||
<div className="summary-details-v2">
|
<div className="summary-details-v2">
|
||||||
<h3 className="summary-theme-name-v2">{res.themeName}</h3>
|
<h3 className="summary-theme-name-v2">{res.themeName}</h3>
|
||||||
<p className="summary-datetime-v2">{formatCardDateTime(res.date, res.startAt)}</p>
|
<p className="summary-datetime-v2">{formatCardDateTime(res.date, res.startAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleShowDetail(res.id)}
|
onClick={() => handleShowDetail(res.id, res.themeName, res.date, res.startAt)}
|
||||||
disabled={isDetailLoading}
|
disabled={isDetailLoading}
|
||||||
className="detail-button-v2"
|
className="detail-button-v2"
|
||||||
>
|
>
|
||||||
|
|||||||
121
frontend/src/pages/v2/ReservationFormPage.tsx
Normal file
121
frontend/src/pages/v2/ReservationFormPage.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { isLoginRequiredError } from '@_api/apiClient';
|
||||||
|
import { createPendingReservation } from '@_api/reservation/reservationAPIV2';
|
||||||
|
import '@_css/reservation-v2-1.css';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
|
||||||
|
|
||||||
|
const ReservationFormPage: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const { scheduleId, theme, date, time } = location.state || {};
|
||||||
|
|
||||||
|
const [reserverName, setReserverName] = useState('');
|
||||||
|
const [reserverContact, setReserverContact] = useState('');
|
||||||
|
const [participantCount, setParticipantCount] = useState(2);
|
||||||
|
const [requirement, setRequirement] = useState('');
|
||||||
|
|
||||||
|
const handleError = (err: any) => {
|
||||||
|
if (isLoginRequiredError(err)) {
|
||||||
|
alert('로그인이 필요해요.');
|
||||||
|
navigate('/login', { state: { from: location } });
|
||||||
|
} else {
|
||||||
|
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
|
||||||
|
alert(message);
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCountChange = (delta: number) => {
|
||||||
|
setParticipantCount(prev => Math.max(theme.minParticipants, Math.min(theme.maxParticipants, prev + delta)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePayment = () => {
|
||||||
|
if (!reserverName || !reserverContact) {
|
||||||
|
alert('예약자명과 연락처를 입력해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reservationData = {
|
||||||
|
scheduleId,
|
||||||
|
reserverName,
|
||||||
|
reserverContact,
|
||||||
|
participantCount,
|
||||||
|
requirement,
|
||||||
|
};
|
||||||
|
|
||||||
|
createPendingReservation(reservationData)
|
||||||
|
.then(res => {
|
||||||
|
navigate('/v2-1/reservation/payment', {
|
||||||
|
state: {
|
||||||
|
reservationId: res.id,
|
||||||
|
themeName: theme.name,
|
||||||
|
date: date,
|
||||||
|
startAt: time,
|
||||||
|
price: theme.price * participantCount,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(handleError);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!scheduleId || !theme) {
|
||||||
|
// Handle case where state is not passed correctly
|
||||||
|
return (
|
||||||
|
<div className="reservation-v21-container">
|
||||||
|
<h2 className="page-title">잘못된 접근</h2>
|
||||||
|
<p>예약 정보가 올바르지 않습니다. 예약 페이지로 다시 이동해주세요.</p>
|
||||||
|
<button onClick={() => navigate('/v2-1/reservation')} className="next-step-button">예약 페이지로</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="reservation-v21-container">
|
||||||
|
<h2 className="page-title">예약 정보 입력</h2>
|
||||||
|
|
||||||
|
<div className="step-section">
|
||||||
|
<h3>예약 내용 확인</h3>
|
||||||
|
<p><strong>테마:</strong> {theme.name}</p>
|
||||||
|
<p><strong>날짜:</strong> {formatDate(date)}</p>
|
||||||
|
<p><strong>시간:</strong> {formatTime(time)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="step-section">
|
||||||
|
<h3>예약자 정보</h3>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="reserverName">예약자명</label>
|
||||||
|
<input type="text" id="reserverName" value={reserverName} onChange={e => setReserverName(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="reserverContact">연락처</label>
|
||||||
|
<input type="tel" id="reserverContact" value={reserverContact} onChange={e => setReserverContact(e.target.value)} placeholder="'-' 없이 입력"/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>인원</label>
|
||||||
|
<div className="participant-control">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={participantCount}
|
||||||
|
onChange={e => setParticipantCount(Math.max(theme.minParticipants, Math.min(theme.maxParticipants, Number(e.target.value))))}
|
||||||
|
min={theme.minParticipants}
|
||||||
|
max={theme.maxParticipants}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="requirement">요청사항</label>
|
||||||
|
<textarea id="requirement" value={requirement} onChange={e => setRequirement(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="next-step-button-container">
|
||||||
|
<button onClick={handlePayment} className="next-step-button">
|
||||||
|
결제하기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReservationFormPage;
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
import { isLoginRequiredError } from '@_api/apiClient';
|
||||||
import { createPendingReservation } from '@_api/reservation/reservationAPI';
|
import { holdSchedule, findAvailableThemesByDate, findSchedules } from '@_api/schedule/scheduleAPI';
|
||||||
import { findAvailableThemesByDate, findSchedules } from '@_api/schedule/scheduleAPI';
|
import { ScheduleStatus, type ScheduleRetrieveResponse } from '@_api/schedule/scheduleTypes';
|
||||||
import type { ScheduleRetrieveResponse } from '@_api/schedule/scheduleTypes';
|
|
||||||
import { findThemesByIds } from '@_api/theme/themeAPI';
|
import { findThemesByIds } from '@_api/theme/themeAPI';
|
||||||
import { Difficulty } from '@_api/theme/themeTypes';
|
import { Difficulty } from '@_api/theme/themeTypes';
|
||||||
import '@_css/reservation-v2-1.css';
|
import '@_css/reservation-v2-1.css';
|
||||||
@ -68,7 +67,9 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
const dateStr = selectedDate.toLocaleDateString('en-CA'); // yyyy-mm-dd
|
const dateStr = selectedDate.toLocaleDateString('en-CA'); // yyyy-mm-dd
|
||||||
findAvailableThemesByDate(dateStr)
|
findAvailableThemesByDate(dateStr)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
console.log('Available themes response:', res);
|
||||||
const themeIds: string[] = res.themeIds;
|
const themeIds: string[] = res.themeIds;
|
||||||
|
console.log('Available theme IDs:', themeIds);
|
||||||
if (themeIds.length > 0) {
|
if (themeIds.length > 0) {
|
||||||
return findThemesByIds({ themeIds });
|
return findThemesByIds({ themeIds });
|
||||||
} else {
|
} else {
|
||||||
@ -104,23 +105,26 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
alert('날짜, 테마, 시간을 모두 선택해주세요.');
|
alert('날짜, 테마, 시간을 모두 선택해주세요.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (selectedSchedule.status !== 'AVAILABLE') {
|
if (selectedSchedule.status !== ScheduleStatus.AVAILABLE) {
|
||||||
alert('예약할 수 없는 시간입니다.');
|
alert('예약할 수 없는 시간입니다.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsConfirmModalOpen(true);
|
setIsConfirmModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmPayment = () => {
|
const handleConfirmReservation = () => {
|
||||||
if (!selectedDate || !selectedTheme || !selectedSchedule) return;
|
if (!selectedSchedule) return;
|
||||||
|
|
||||||
const reservationData = {
|
holdSchedule(selectedSchedule.id)
|
||||||
scheduleId: selectedSchedule.id,
|
.then(() => {
|
||||||
};
|
navigate('/v2/reservation/form', {
|
||||||
|
state: {
|
||||||
createPendingReservation(reservationData)
|
scheduleId: selectedSchedule.id,
|
||||||
.then((res) => {
|
theme: selectedTheme,
|
||||||
navigate('/v2-1/reservation/payment', { state: { reservation: res } });
|
date: selectedDate.toLocaleDateString('en-CA'),
|
||||||
|
time: selectedSchedule.time,
|
||||||
|
}
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(handleError)
|
.catch(handleError)
|
||||||
.finally(() => setIsConfirmModalOpen(false));
|
.finally(() => setIsConfirmModalOpen(false));
|
||||||
@ -197,8 +201,19 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
setSelectedTheme(theme);
|
setSelectedTheme(theme);
|
||||||
setIsThemeModalOpen(true);
|
setIsThemeModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getStatusText = (status: ScheduleStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case ScheduleStatus.AVAILABLE:
|
||||||
|
return '예약가능';
|
||||||
|
case ScheduleStatus.HOLD:
|
||||||
|
return '예약 진행중';
|
||||||
|
default:
|
||||||
|
return '예약불가';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isButtonDisabled = !selectedDate || !selectedTheme || !selectedSchedule || selectedSchedule.status !== 'AVAILABLE';
|
const isButtonDisabled = !selectedDate || !selectedTheme || !selectedSchedule || selectedSchedule.status !== ScheduleStatus.AVAILABLE;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="reservation-v21-container">
|
<div className="reservation-v21-container">
|
||||||
@ -221,7 +236,7 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
<div className="theme-info">
|
<div className="theme-info">
|
||||||
<h4>{theme.name}</h4>
|
<h4>{theme.name}</h4>
|
||||||
<div className="theme-meta">
|
<div className="theme-meta">
|
||||||
<p><strong>가격:</strong> {theme.price.toLocaleString()}원</p>
|
<p><strong>1인당 요금:</strong> {theme.price.toLocaleString()}원</p>
|
||||||
<p><strong>난이도:</strong> {getDifficultyText(theme.difficulty)}</p>
|
<p><strong>난이도:</strong> {getDifficultyText(theme.difficulty)}</p>
|
||||||
<p><strong>참여 가능 인원:</strong> {theme.minParticipants} ~ {theme.maxParticipants}명</p>
|
<p><strong>참여 가능 인원:</strong> {theme.minParticipants} ~ {theme.maxParticipants}명</p>
|
||||||
<p><strong>예상 소요 시간:</strong> {theme.expectedMinutesFrom} ~ {theme.expectedMinutesTo}분</p>
|
<p><strong>예상 소요 시간:</strong> {theme.expectedMinutesFrom} ~ {theme.expectedMinutesTo}분</p>
|
||||||
@ -240,11 +255,11 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
{schedules.length > 0 ? schedules.map(schedule => (
|
{schedules.length > 0 ? schedules.map(schedule => (
|
||||||
<div
|
<div
|
||||||
key={schedule.id}
|
key={schedule.id}
|
||||||
className={`time-slot ${selectedSchedule?.id === schedule.id ? 'active' : ''} ${schedule.status !== 'AVAILABLE' ? 'disabled' : ''}`}
|
className={`time-slot ${selectedSchedule?.id === schedule.id ? 'active' : ''} ${schedule.status !== ScheduleStatus.AVAILABLE ? 'disabled' : ''}`}
|
||||||
onClick={() => schedule.status === 'AVAILABLE' && setSelectedSchedule(schedule)}
|
onClick={() => schedule.status === ScheduleStatus.AVAILABLE && setSelectedSchedule(schedule)}
|
||||||
>
|
>
|
||||||
{schedule.time}
|
{schedule.time}
|
||||||
<span className="time-availability">{schedule.status === 'AVAILABLE' ? '예약가능' : '예약불가'}</span>
|
<span className="time-availability">{getStatusText(schedule.status)}</span>
|
||||||
</div>
|
</div>
|
||||||
)) : <div className="no-times">선택 가능한 시간이 없습니다.</div>}
|
)) : <div className="no-times">선택 가능한 시간이 없습니다.</div>}
|
||||||
</div>
|
</div>
|
||||||
@ -252,7 +267,7 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
|
|
||||||
<div className="next-step-button-container">
|
<div className="next-step-button-container">
|
||||||
<button className="next-step-button" disabled={isButtonDisabled} onClick={handleNextStep}>
|
<button className="next-step-button" disabled={isButtonDisabled} onClick={handleNextStep}>
|
||||||
결제하기
|
예약하기
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -267,7 +282,7 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
<p><strong>난이도:</strong> {getDifficultyText(selectedTheme.difficulty)}</p>
|
<p><strong>난이도:</strong> {getDifficultyText(selectedTheme.difficulty)}</p>
|
||||||
<p><strong>참여 인원:</strong> {selectedTheme.minParticipants} ~ {selectedTheme.maxParticipants}명</p>
|
<p><strong>참여 인원:</strong> {selectedTheme.minParticipants} ~ {selectedTheme.maxParticipants}명</p>
|
||||||
<p><strong>소요 시간:</strong> {selectedTheme.expectedMinutesFrom} ~ {selectedTheme.expectedMinutesTo}분</p>
|
<p><strong>소요 시간:</strong> {selectedTheme.expectedMinutesFrom} ~ {selectedTheme.expectedMinutesTo}분</p>
|
||||||
<p><strong>가격:</strong> {selectedTheme.price.toLocaleString()}원</p>
|
<p><strong>1인당 요금:</strong> {selectedTheme.price.toLocaleString()}원</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-section">
|
<div className="modal-section">
|
||||||
<h3>소개</h3>
|
<h3>소개</h3>
|
||||||
@ -286,11 +301,10 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
<p><strong>날짜:</strong> {formatDate(selectedDate!!.toLocaleDateString('ko-KR'))}</p>
|
<p><strong>날짜:</strong> {formatDate(selectedDate!!.toLocaleDateString('ko-KR'))}</p>
|
||||||
<p><strong>테마:</strong> {selectedTheme!!.name}</p>
|
<p><strong>테마:</strong> {selectedTheme!!.name}</p>
|
||||||
<p><strong>시간:</strong> {formatTime(selectedSchedule!!.time)}</p>
|
<p><strong>시간:</strong> {formatTime(selectedSchedule!!.time)}</p>
|
||||||
<p><strong>결제금액:</strong> {selectedTheme!!.price.toLocaleString()}원</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-actions">
|
<div className="modal-actions">
|
||||||
<button className="cancel-button" onClick={() => setIsConfirmModalOpen(false)}>취소</button>
|
<button className="cancel-button" onClick={() => setIsConfirmModalOpen(false)}>취소</button>
|
||||||
<button className="confirm-button" onClick={handleConfirmPayment}>결제하기</button>
|
<button className="confirm-button" onClick={handleConfirmReservation}>예약하기</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
|
import { isLoginRequiredError } from '@_api/apiClient';
|
||||||
|
import { confirmReservationPayment } from '@_api/reservation/reservationAPI';
|
||||||
|
import { type ReservationCreateResponse, type ReservationPaymentRequest } from '@_api/reservation/reservationTypes';
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { confirmReservationPayment } from '@_api/reservation/reservationAPI';
|
|
||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
import { PaymentType } from '@_api/payment/PaymentTypes';
|
||||||
import { PaymentType, type ReservationCreateResponse, type ReservationPaymentRequest } from '@_api/reservation/reservationTypes';
|
|
||||||
import '@_css/reservation-v2.css';
|
import '@_css/reservation-v2.css';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@ -49,7 +51,7 @@ const ReservationStep2Page: React.FC = () => {
|
|||||||
|
|
||||||
const paymentMethods = paymentWidget.renderPaymentMethods(
|
const paymentMethods = paymentWidget.renderPaymentMethods(
|
||||||
"#payment-method",
|
"#payment-method",
|
||||||
{ value: 1000 }, // TODO: 테마별 가격 적용
|
{ value: 1000 }, // TODO: 테마별 요금 적용
|
||||||
{ variantKey: "DEFAULT" }
|
{ variantKey: "DEFAULT" }
|
||||||
);
|
);
|
||||||
paymentMethodsRef.current = paymentMethods;
|
paymentMethodsRef.current = paymentMethods;
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
import { isLoginRequiredError } from '@_api/apiClient';
|
||||||
import { confirmReservationPayment } from '@_api/reservation/reservationAPI';
|
import { confirmPayment } from '@_api/payment/paymentAPI';
|
||||||
import { PaymentType, type ReservationCreateResponse, type ReservationPaymentRequest } from '@_api/reservation/reservationTypes';
|
import { PaymentType, type PaymentConfirmRequest } from '@_api/payment/PaymentTypes';
|
||||||
import '@_css/reservation-v2-1.css'; // Reuse the new CSS for consistency
|
import { confirmReservation } from '@_api/reservation/reservationAPIV2';
|
||||||
|
import '@_css/reservation-v2-1.css';
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
|
import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
|
||||||
@ -12,16 +13,14 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This component is designed to work with the state passed from ReservationStep1PageV21
|
|
||||||
const ReservationStep2PageV21: React.FC = () => {
|
const ReservationStep2PageV21: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const paymentWidgetRef = useRef<any>(null);
|
const paymentWidgetRef = useRef<any>(null);
|
||||||
const paymentMethodsRef = useRef<any>(null);
|
const paymentMethodsRef = useRef<any>(null);
|
||||||
|
|
||||||
// The reservation object now contains the price
|
const { reservationId, themeName, date, startAt, price } = location.state || {};
|
||||||
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('로그인이 필요해요.');
|
||||||
@ -34,7 +33,7 @@ const ReservationStep2PageV21: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!reservation) {
|
if (!reservationId) {
|
||||||
alert('잘못된 접근입니다.');
|
alert('잘못된 접근입니다.');
|
||||||
navigate('/v2-1/reservation');
|
navigate('/v2-1/reservation');
|
||||||
return;
|
return;
|
||||||
@ -52,15 +51,15 @@ const ReservationStep2PageV21: React.FC = () => {
|
|||||||
|
|
||||||
const paymentMethods = paymentWidget.renderPaymentMethods(
|
const paymentMethods = paymentWidget.renderPaymentMethods(
|
||||||
"#payment-method",
|
"#payment-method",
|
||||||
{ value: reservation.price }, // Use the price from the reservation object
|
{ value: price },
|
||||||
{ variantKey: "DEFAULT" }
|
{ variantKey: "DEFAULT" }
|
||||||
);
|
);
|
||||||
paymentMethodsRef.current = paymentMethods;
|
paymentMethodsRef.current = paymentMethods;
|
||||||
};
|
};
|
||||||
}, [reservation, navigate]);
|
}, [reservationId, price, navigate]);
|
||||||
|
|
||||||
const handlePayment = () => {
|
const handlePayment = () => {
|
||||||
if (!paymentWidgetRef.current || !reservation) {
|
if (!paymentWidgetRef.current || !reservationId) {
|
||||||
alert('결제 위젯이 로드되지 않았거나 예약 정보가 없습니다.');
|
alert('결제 위젯이 로드되지 않았거나 예약 정보가 없습니다.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -70,24 +69,27 @@ const ReservationStep2PageV21: React.FC = () => {
|
|||||||
|
|
||||||
paymentWidgetRef.current.requestPayment({
|
paymentWidgetRef.current.requestPayment({
|
||||||
orderId: generateRandomString(),
|
orderId: generateRandomString(),
|
||||||
orderName: `${reservation.themeName} 예약 결제`,
|
orderName: `${themeName} 예약 결제`,
|
||||||
amount: reservation.price,
|
amount: price,
|
||||||
}).then((data: any) => {
|
}).then((data: any) => {
|
||||||
const paymentData: ReservationPaymentRequest = {
|
const paymentData: PaymentConfirmRequest = {
|
||||||
paymentKey: data.paymentKey,
|
paymentKey: data.paymentKey,
|
||||||
orderId: data.orderId,
|
orderId: data.orderId,
|
||||||
amount: data.amount,
|
amount: price, // Use the price from component state instead of widget response
|
||||||
paymentType: data.paymentType || PaymentType.NORMAL,
|
paymentType: data.paymentType || PaymentType.NORMAL,
|
||||||
};
|
};
|
||||||
confirmReservationPayment(reservation.reservationId, paymentData)
|
|
||||||
.then((res) => {
|
confirmPayment(reservationId, paymentData)
|
||||||
// Navigate to the new success page
|
.then(() => {
|
||||||
|
return confirmReservation(reservationId);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
alert('결제가 완료되었어요!');
|
||||||
navigate('/v2-1/reservation/success', {
|
navigate('/v2-1/reservation/success', {
|
||||||
state: {
|
state: {
|
||||||
reservation: res,
|
themeName,
|
||||||
themeName: reservation.themeName,
|
date,
|
||||||
date: reservation.date,
|
startAt,
|
||||||
startAt: reservation.startAt,
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -98,22 +100,19 @@ const ReservationStep2PageV21: React.FC = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!reservation) {
|
if (!reservationId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = formatDate(reservation.date)
|
|
||||||
const time = formatTime(reservation.startAt);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="reservation-v21-container">
|
<div className="reservation-v21-container">
|
||||||
<h2 className="page-title">결제하기</h2>
|
<h2 className="page-title">결제하기</h2>
|
||||||
<div className="step-section">
|
<div className="step-section">
|
||||||
<h3>결제 정보 확인</h3>
|
<h3>결제 정보 확인</h3>
|
||||||
<p><strong>테마:</strong> {reservation.themeName}</p>
|
<p><strong>테마:</strong> {themeName}</p>
|
||||||
<p><strong>날짜:</strong> {date}</p>
|
<p><strong>날짜:</strong> {formatDate(date)}</p>
|
||||||
<p><strong>시간:</strong> {time}</p>
|
<p><strong>시간:</strong> {formatTime(startAt)}</p>
|
||||||
<p><strong>금액:</strong> {reservation.price.toLocaleString()}원</p>
|
<p><strong>금액:</strong> {price.toLocaleString()}원</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="step-section">
|
<div className="step-section">
|
||||||
<h3>결제 수단</h3>
|
<h3>결제 수단</h3>
|
||||||
@ -122,7 +121,7 @@ const ReservationStep2PageV21: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="next-step-button-container">
|
<div className="next-step-button-container">
|
||||||
<button onClick={handlePayment} className="next-step-button">
|
<button onClick={handlePayment} className="next-step-button">
|
||||||
{reservation.price.toLocaleString()}원 결제하기
|
{price.toLocaleString()}원 결제하기
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,25 +1,16 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useLocation, useNavigate, Link } from 'react-router-dom';
|
|
||||||
import type { ReservationPaymentResponse } from '@_api/reservation/reservationTypes';
|
|
||||||
import '@_css/reservation-v2-1.css'; // Reuse the new CSS
|
import '@_css/reservation-v2-1.css'; // Reuse the new CSS
|
||||||
|
import React from 'react';
|
||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
|
import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
|
||||||
|
|
||||||
const ReservationSuccessPageV21: React.FC = () => {
|
const ReservationSuccessPageV21: React.FC = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const { themeName, date, startAt } = (location.state as {
|
||||||
const { reservation, themeName, date, startAt } = (location.state as {
|
|
||||||
reservation: ReservationPaymentResponse;
|
|
||||||
themeName: string;
|
themeName: string;
|
||||||
date: string;
|
date: string;
|
||||||
startAt: string;
|
startAt: string;
|
||||||
}) || {};
|
}) || {};
|
||||||
|
|
||||||
if (!reservation) {
|
|
||||||
React.useEffect(() => {
|
|
||||||
navigate('/v2-1/reservation'); // Redirect to the new reservation page on error
|
|
||||||
}, [navigate]);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const formattedDate = formatDate(date)
|
const formattedDate = formatDate(date)
|
||||||
const formattedTime = formatTime(startAt);
|
const formattedTime = formatTime(startAt);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user