[#35] 결제 스키마 재정의 & 예약 조회 페이지 개선 #36

Merged
pricelees merged 37 commits from refactor/#35 into main 2025-08-22 06:43:16 +00:00
9 changed files with 134 additions and 348 deletions
Showing only changes of commit 2e2b71743f - Show all commits

View File

@ -2,14 +2,16 @@ import apiClient from "@_api/apiClient";
import type { import type {
AdminReservationCreateRequest, AdminReservationCreateRequest,
MyReservationRetrieveListResponse, MyReservationRetrieveListResponse,
ReservationPaymentRequest,
ReservationPaymentResponse,
ReservationCreateRequest, ReservationCreateRequest,
ReservationCreateResponse, ReservationCreateResponse,
ReservationCreateWithPaymentRequest, ReservationCreateWithPaymentRequest,
ReservationDetailV2,
ReservationPaymentRequest,
ReservationPaymentResponse,
ReservationRetrieveListResponse, ReservationRetrieveListResponse,
ReservationRetrieveResponse, ReservationRetrieveResponse,
ReservationSearchQuery, ReservationSearchQuery,
ReservationSummaryListV2,
WaitingCreateRequest WaitingCreateRequest
} from "./reservationTypes"; } from "./reservationTypes";
@ -82,3 +84,18 @@ export const createPendingReservation = async (data: ReservationCreateRequest):
export const confirmReservationPayment = async (id: string, data: ReservationPaymentRequest): Promise<ReservationPaymentResponse> => { export const confirmReservationPayment = async (id: string, data: ReservationPaymentRequest): Promise<ReservationPaymentResponse> => {
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
export const fetchMyReservationsV2 = async (): Promise<ReservationSummaryListV2> => {
return await apiClient.get<ReservationSummaryListV2>('/v2/reservations', true);
};
// GET /v2/reservations/{id}/details
export const fetchReservationDetailV2 = async (id: string): Promise<ReservationDetailV2> => {
return await apiClient.get<ReservationDetailV2>(`/v2/reservations/${id}/details`, true);
};

View File

@ -1,150 +0,0 @@
import type { ReservationSummaryV2, ReservationDetailV2 } from './reservationTypes';
// --- API 호출 함수 ---
/**
* . (V2)
*/
export const fetchMyReservationsV2 = async (): Promise<ReservationSummaryV2[]> => {
// 실제 API 연동 시 아래 코드로 교체
// const response = await apiClient.get<ReservationSummaryV2[]>('/v2/reservations');
// return response.data;
// 현재는 목업 데이터를 반환합니다.
console.log('[API] fetchMyReservationsV2 호출');
return new Promise((resolve) =>
setTimeout(() => {
resolve([
{ id: 1, date: '2025-08-20', time: '14:00', themeName: '공포의 방', status: 'CONFIRMED' },
{ id: 2, date: '2025-08-22', time: '19:30', themeName: '신비의 숲', status: 'CONFIRMED' },
{ id: 3, date: '2024-12-25', time: '11:00', themeName: '미래 도시', status: 'CANCELLED' },
]);
}, 500)
);
};
/**
* . (V2)
* @param id ID
*/
export const fetchReservationDetailV2 = async (id: number): Promise<ReservationDetailV2> => {
// 실제 API 연동 시 아래 코드로 교체
// const response = await apiClient.get<ReservationDetailV2>(`/v2/reservations/${id}`);
// return response.data;
// 현재는 목업 데이터를 반환합니다.
console.log(`[API] fetchReservationDetailV2 호출 (id: ${id})`);
console.log(`[API] fetchReservationDetailV2 호출 (id: ${id})`);
const mockDetails: { [key: number]: ReservationDetailV2 } = {
1: {
id: 1,
memberName: '박예약',
memberEmail: 'reserve@example.com',
applicationDateTime: '2025-08-20T13:50:00Z',
payment: {
paymentKey: 'reserve-payment-key',
orderId: 'reserve-order-id',
totalAmount: 50000,
method: 'CARD',
status: 'DONE',
requestedAt: '2025-08-20T13:50:00Z',
approvedAt: '2025-08-20T13:50:05Z',
detail: {
type: 'CARD',
issuerCode: 'SHINHAN',
cardType: 'CHECK',
ownerType: 'PERSONAL',
cardNumber: '5423-****-****-1234',
approvalNumber: '12345678',
installmentPlanMonths: 0,
isInterestFree: true,
}
},
cancellation: null,
},
2: {
id: 2,
memberName: '이간편',
memberEmail: 'easypay@example.com',
applicationDateTime: '2025-08-22T19:20:00Z',
payment: {
paymentKey: 'easypay-card-key',
orderId: 'easypay-card-order-id',
totalAmount: 75000,
method: 'CARD',
status: 'DONE',
requestedAt: '2025-08-22T19:20:00Z',
approvedAt: '2025-08-22T19:20:05Z',
detail: {
type: 'CARD',
issuerCode: 'HYUNDAI',
cardType: 'CREDIT',
ownerType: 'PERSONAL',
cardNumber: '4321-****-****-5678',
approvalNumber: '87654321',
installmentPlanMonths: 3,
isInterestFree: false,
easypayProviderCode: 'NAVERPAY',
}
},
cancellation: null,
},
3: {
id: 3,
memberName: '김취소',
memberEmail: 'cancel@example.com',
applicationDateTime: '2024-12-25T10:50:00Z',
payment: {
paymentKey: 'cancel-payment-key',
orderId: 'cancel-order-id',
totalAmount: 52000,
method: 'EASYPAY_PREPAID',
status: 'CANCELED',
requestedAt: '2024-12-25T10:50:00Z',
approvedAt: '2024-12-25T10:50:05Z',
detail: {
type: 'EASYPAY_PREPAID',
easypayProviderCode: 'TOSSPAY',
amount: 52000,
discountAmount: 0,
}
},
cancellation: {
cancellationRequestedAt: '2025-01-10T14:59:00Z',
cancellationCompletedAt: '2025-01-10T15:00:00Z',
cancelReason: '개인 사정으로 인한 취소',
},
}
};
return new Promise((resolve) =>
setTimeout(() => {
const mockDetail = mockDetails[id] || mockDetails[1]; // Fallback to 1
resolve(mockDetail);
}, 800)
);
};
/**
* . (V2)
* @param id ID
* @param cancelReason
*/
export const cancelReservationV2 = async (id: number, cancelReason: string): Promise<void> => {
// 실제 API 연동 시 아래 코드로 교체
// await apiClient.post(`/v2/reservations/${id}/cancel`, { cancelReason });
console.log(`[API] cancelReservationV2 호출 (id: ${id}, reason: ${cancelReason})`);
return new Promise((resolve, reject) => {
setTimeout(() => {
if (cancelReason && cancelReason.trim().length > 0) {
// 성공 시, 목업 데이터 업데이트 (실제로는 서버가 처리)
console.log(`Reservation ${id} has been cancelled.`);
resolve();
} else {
reject(new Error('취소 사유를 반드시 입력해야 합니다.'));
}
}, 800);
});
};

View File

@ -134,27 +134,38 @@ export interface ReservationPaymentResponse {
} }
export interface ReservationSummaryV2 { export interface ReservationSummaryV2 {
id: number; id: string;
themeName: string; themeName: string;
date: string; date: string;
time: string; startAt: string;
status: 'CONFIRMED' | 'CANCELLED'; status: string; // 'CONFIRMED', 'CANCELED_BY_USER', etc.
}
export interface ReservationSummaryListV2 {
reservations: ReservationSummaryV2[];
} }
export interface ReservationDetailV2 { export interface ReservationDetailV2 {
id: number; id: string;
memberName: string; user: UserDetailV2;
memberEmail: string; themeName: string;
applicationDateTime: string; // yyyy년 MM월 dd일 HH시 mm분 date: string;
startAt: string;
applicationDateTime: string;
payment: PaymentV2; payment: PaymentV2;
cancellation: CancellationV2 | null; cancellation: CancellationV2 | null;
} }
export interface UserDetailV2 {
id: string;
name: string;
email: string;
}
export interface PaymentV2 { export interface PaymentV2 {
paymentKey: string;
orderId: string; orderId: string;
totalAmount: number; totalAmount: number;
method: 'CARD' | 'BANK_TRANSFER' | 'EASYPAY_PREPAID'; method: string;
status: 'DONE' | 'CANCELED'; status: 'DONE' | 'CANCELED';
requestedAt: string; requestedAt: string;
approvedAt: string; approvedAt: string;
@ -167,28 +178,30 @@ export interface CardPaymentDetailV2 {
cardType: 'CREDIT' | 'CHECK' | 'GIFT'; cardType: 'CREDIT' | 'CHECK' | 'GIFT';
ownerType: 'PERSONAL' | 'CORPORATE'; ownerType: 'PERSONAL' | 'CORPORATE';
cardNumber: string; cardNumber: string;
amount: number;
approvalNumber: string; approvalNumber: string;
installmentPlanMonths: number; installmentPlanMonths: number;
isInterestFree: boolean; isInterestFree: boolean;
easypayProviderCode?: string; easypayProviderName?: string;
easypayDiscountAmount?: number; easypayDiscountAmount?: number;
} }
export interface BankTransferPaymentDetailV2 { export interface BankTransferPaymentDetailV2 {
type: 'BANK_TRANSFER'; type: 'BANK_TRANSFER';
bankCode: string; bankName: string;
settlementStatus: string; settlementStatus: string;
} }
export interface EasyPayPrepaidPaymentDetailV2 { export interface EasyPayPrepaidPaymentDetailV2 {
type: 'EASYPAY_PREPAID'; type: 'EASYPAY_PREPAID';
easypayProviderCode: string; providerName: string;
amount: number; amount: number;
discountAmount: number; discountAmount: number;
} }
export interface CancellationV2 { export interface CancellationV2 {
cancellationRequestedAt: string; // ISO 8601 format cancellationRequestedAt: string; // ISO 8601 format
cancellationCompletedAt: string; // ISO 8601 format cancellationApprovedAt: string; // ISO 8601 format
cancelReason: string; cancelReason: string;
canceledBy: string;
} }

View File

@ -27,7 +27,7 @@ const Navbar: React.FC = () => {
<div className="collapse navbar-collapse" id="navbarSupportedContent"> <div className="collapse navbar-collapse" id="navbarSupportedContent">
<ul className="navbar-nav ms-auto"> <ul className="navbar-nav ms-auto">
<li className="nav-item"> <li className="nav-item">
<Link className="nav-link" to="/reservation">Reservation</Link> <Link className="nav-link" to="/v2/reservation">Reservation</Link>
</li> </li>
{!loggedIn ? ( {!loggedIn ? (
<li className="nav-item"> <li className="nav-item">
@ -40,7 +40,7 @@ const Navbar: React.FC = () => {
<span id="profile-name">{userName}</span> <span id="profile-name">{userName}</span>
</a> </a>
<ul className="dropdown-menu" aria-labelledby="navbarDropdown"> <ul className="dropdown-menu" aria-labelledby="navbarDropdown">
<li><Link className="dropdown-item" to="/reservation-mine">My Reservation</Link></li> <li><Link className="dropdown-item" to="/my-reservation/v2">My Reservation</Link></li>
<li><hr className="dropdown-divider" /></li> <li><hr className="dropdown-divider" /></li>
<li><a className="dropdown-item" href="#" onClick={handleLogout}>Logout</a></li> <li><a className="dropdown-item" href="#" onClick={handleLogout}>Logout</a></li>
</ul> </ul>

View File

@ -1,137 +0,0 @@
import React, { useState } from 'react';
import '../css/my-reservation-v2.css';
import type { Reservation } from '../pages/v2/MyReservationPageV2'; // 페이지로부터 타입 import
interface ReservationCardProps {
reservation: Reservation;
}
// 날짜 및 시간 포맷팅 함수
const formatDateTime = (dateStr: string, timeStr: string): string => {
const date = new Date(`${dateStr}T${timeStr}`);
const today = new Date();
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const dayOfWeek = ['일', '월', '화', '수', '목', '금', '토'][date.getDay()];
let hour = date.getHours();
const minute = date.getMinutes();
const isPastYear = year < today.getFullYear();
const ampm = hour < 12 ? '오전' : '오후';
hour = hour % 12;
if (hour === 0) hour = 12;
let datePart = '';
if (isPastYear) {
datePart = `${String(year).slice(-2)}.${month}.${day}`;
} else {
datePart = `${month}.${day}`;
}
let timePart = `${ampm} ${hour}`;
if (minute > 0) {
timePart += ` ${minute}`;
}
return `${datePart}(${dayOfWeek}) ${timePart}`;
};
const ReservationCard: React.FC<ReservationCardProps> = ({ reservation }) => {
const [isExpanded, setIsExpanded] = useState(false);
const [isCancelling, setIsCancelling] = useState(false);
const [cancelReason, setCancelReason] = useState('');
const isCanceled = reservation.status === 'CANCELED';
const handleToggleDetails = () => {
setIsExpanded(!isExpanded);
if (isExpanded) {
setIsCancelling(false);
setCancelReason('');
}
};
const handleStartCancel = () => {
if (window.confirm('정말 예약을 취소하시겠어요?')) {
setIsCancelling(true);
}
};
const handleSubmitCancellation = () => {
console.log('--- 예약 취소 요청 ---');
console.log('예약 ID:', reservation.id);
console.log('취소 사유:', cancelReason);
alert(`취소 요청을 접수했어요.`);
setIsCancelling(false);
setIsExpanded(false);
setCancelReason('');
// 실제 API 연동 시에는 상태를 다시 fetch 해야 함
};
const cardClasses = `reservation-card ${isCanceled ? 'canceled' : ''}`;
const headerClasses = `theme-header ${isCanceled ? 'canceled' : ''}`;
return (
<div className={cardClasses}>
{isCanceled && <div className="status-badge"> </div>}
<div className="reservation-summary" onClick={handleToggleDetails}>
<div className="summary-info">
<h4 className={headerClasses}>{reservation.theme}</h4>
<span className="date-time">{formatDateTime(reservation.date, reservation.time)}</span>
</div>
<button className="details-button" type="button">
{isExpanded ? '닫기' : '상세보기'}
</button>
</div>
{isExpanded && (
<div className="reservation-details">
<div className="details-item">
<span className="label"></span>
<span className="value">{reservation.payment.method}</span>
</div>
<div className="details-item">
<span className="label"></span>
<span className="value">{reservation.payment.amount}</span>
</div>
<div className="cancel-section">
{isCanceled ? (
<button className="cancel-button disabled" type="button" disabled>
</button>
) : !isCancelling ? (
<button className="cancel-button" type="button" onClick={handleStartCancel}>
</button>
) : (
<>
<textarea
className="cancel-reason-input"
placeholder="취소 사유를 입력해주세요."
value={cancelReason}
onChange={(e) => setCancelReason(e.target.value)}
/>
<button
className="submit-cancel-button"
type="button"
onClick={handleSubmitCancellation}
disabled={!cancelReason.trim()}
>
</button>
</>
)}
</div>
</div>
)}
</div>
);
};
export default ReservationCard;

View File

@ -69,14 +69,14 @@
} }
/* Canceled Card Style */ /* Canceled Card Style */
.reservation-summary-card-v2.status-cancelled { .reservation-summary-card-v2.status-canceled_by_user {
background-color: #f8f9fa; background-color: #f8f9fa;
opacity: 0.6; opacity: 0.6;
} }
.reservation-summary-card-v2.status-cancelled .summary-theme-name-v2, .reservation-summary-card-v2.status-canceled_by_user .summary-theme-name-v2,
.reservation-summary-card-v2.status-cancelled .summary-datetime-v2, .reservation-summary-card-v2.status-canceled_by_user .summary-datetime-v2,
.reservation-summary-card-v2.status-cancelled .summary-details-v2 strong { .reservation-summary-card-v2.status-canceled_by_user .summary-details-v2 strong {
color: #6c757d; color: #6c757d;
} }

View File

@ -57,8 +57,8 @@ const AdminReservationPage: React.FC = () => {
const applyFilter = (e: React.FormEvent) => { const applyFilter = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const params = { const params = {
memberId: filter.memberId ? Number(filter.memberId) : undefined, memberId: filter.memberId ? filter.memberId : undefined,
themeId: filter.themeId ? Number(filter.themeId) : undefined, themeId: filter.themeId ? filter.themeId : undefined,
dateFrom: filter.dateFrom, dateFrom: filter.dateFrom,
dateTo: filter.dateTo, dateTo: filter.dateTo,
}; };
@ -76,10 +76,10 @@ const AdminReservationPage: React.FC = () => {
return; return;
} }
const request = { const request = {
memberId: Number(newReservation.memberId), memberId: newReservation.memberId,
themeId: Number(newReservation.themeId), themeId: newReservation.themeId,
date: newReservation.date, date: newReservation.date,
timeId: Number(newReservation.timeId), timeId: newReservation.timeId,
}; };
await createReservationByAdmin(request) await createReservationByAdmin(request)
.then(() => { .then(() => {

View File

@ -1,14 +1,39 @@
import React, { useState, useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import '../../css/my-reservation-v2.css';
import { import {
cancelReservationV2,
fetchMyReservationsV2, fetchMyReservationsV2,
fetchReservationDetailV2, fetchReservationDetailV2
cancelReservationV2 } from '../../api/reservation/reservationAPI';
} from '../../api/reservation/reservationAPIV2'; import type { PaymentV2, ReservationDetailV2, ReservationSummaryV2 } from '../../api/reservation/reservationTypes';
import type { ReservationSummaryV2, ReservationDetailV2, PaymentV2 } from '../../api/reservation/reservationTypes'; import '../../css/my-reservation-v2.css';
const formatDisplayDateTime = (dateTime: any): string => {
let date: Date;
if (typeof dateTime === 'string') {
// ISO 문자열 형식 처리 (LocalDateTime, OffsetDateTime 모두 포함)
date = new Date(dateTime);
} else if (typeof dateTime === 'number') {
// Unix 타임스탬프(초) 형식 처리
date = new Date(dateTime * 1000);
} else if (Array.isArray(dateTime) && dateTime.length >= 6) {
// 배열 형식 처리: [year, month, day, hour, minute, second, nanosecond?]
const year = dateTime[0];
const month = dateTime[1] - 1; // JS Date의 월은 0부터 시작
const day = dateTime[2];
const hour = dateTime[3];
const minute = dateTime[4];
const second = dateTime[5];
const millisecond = dateTime.length > 6 ? Math.floor(dateTime[6] / 1000000) : 0;
date = new Date(year, month, day, hour, minute, second, millisecond);
} else {
return '유효하지 않은 날짜 형식';
}
if (isNaN(date.getTime())) {
return '유효하지 않은 날짜';
}
const formatDisplayDateTime = (isoString: string): string => {
const date = new Date(isoString);
const options: Intl.DateTimeFormatOptions = { const options: Intl.DateTimeFormatOptions = {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
@ -16,6 +41,7 @@ const formatDisplayDateTime = (isoString: string): string => {
hour: 'numeric', hour: 'numeric',
minute: 'numeric', minute: 'numeric',
hour12: true, hour12: true,
second: 'numeric'
}; };
return new Intl.DateTimeFormat('ko-KR', options).format(date); return new Intl.DateTimeFormat('ko-KR', options).format(date);
}; };
@ -71,7 +97,7 @@ const CancellationView: React.FC<{
<div className="cancellation-view-v2"> <div className="cancellation-view-v2">
<h3> </h3> <h3> </h3>
<div className="cancellation-summary-v2"> <div className="cancellation-summary-v2">
<p><strong>:</strong> {reservation.memberName}</p> <p><strong>:</strong> {reservation.themeName}</p>
<p><strong> :</strong> {formatDisplayDateTime(reservation.applicationDateTime)}</p> <p><strong> :</strong> {formatDisplayDateTime(reservation.applicationDateTime)}</p>
<p><strong> :</strong> {reservation.payment.totalAmount.toLocaleString()}</p> <p><strong> :</strong> {reservation.payment.totalAmount.toLocaleString()}</p>
</div> </div>
@ -106,11 +132,22 @@ const ReservationDetailView: React.FC<{
case 'CARD': case 'CARD':
return ( return (
<> <>
<p><strong> :</strong> {payment.totalAmount.toLocaleString()}</p> <p><strong> ID:</strong> {payment.orderId}</p>
<p><strong> :</strong> {detail.easypayProviderCode ? `간편결제 (${detail.easypayProviderCode})` : '카드'}</p> {payment.totalAmount === detail.amount ? (
<p><strong> :</strong> {detail.issuerCode}({detail.ownerType}) / {detail.cardType}</p> <p><strong> :</strong> {payment.totalAmount.toLocaleString()}</p>
) : (
<>
<p><strong> :</strong> {payment.totalAmount.toLocaleString()}</p>
<p><strong> :</strong> {detail.amount.toLocaleString()}</p>
{detail.easypayDiscountAmount && (
<p><strong> :</strong> {detail.easypayDiscountAmount.toLocaleString()}</p>
)}
</>
)}
<p><strong> :</strong> {detail.easypayProviderName ? `간편결제 / ${detail.easypayProviderName}` : '카드'}</p>
<p><strong> / :</strong> {detail.issuerCode}({detail.ownerType}) / {detail.cardType}</p>
<p><strong> :</strong> {detail.cardNumber}</p> <p><strong> :</strong> {detail.cardNumber}</p>
<p><strong> :</strong> {detail.installmentPlanMonths === 0 ? '일시불' : `${detail.installmentPlanMonths}개월`}</p> <p><strong> :</strong> {detail.installmentPlanMonths === 0 ? '일시불' : `${detail.installmentPlanMonths}개월`}</p>
<p><strong> :</strong> {detail.approvalNumber}</p> <p><strong> :</strong> {detail.approvalNumber}</p>
</> </>
); );
@ -118,13 +155,13 @@ const ReservationDetailView: React.FC<{
return ( return (
<> <>
<p><strong> :</strong> </p> <p><strong> :</strong> </p>
<p><strong>:</strong> {detail.bankCode}</p> <p><strong>:</strong> {detail.bankName}</p>
</> </>
); );
case 'EASYPAY_PREPAID': case 'EASYPAY_PREPAID':
return ( return (
<> <>
<p><strong> :</strong> / {detail.easypayProviderCode}</p> <p><strong> :</strong> / {detail.providerName}</p>
<p><strong> :</strong> {payment.totalAmount.toLocaleString()}</p> <p><strong> :</strong> {payment.totalAmount.toLocaleString()}</p>
<p><strong> :</strong> {detail.amount.toLocaleString()}</p> <p><strong> :</strong> {detail.amount.toLocaleString()}</p>
{detail.discountAmount > 0 && <p><strong>:</strong> {detail.discountAmount.toLocaleString()}</p>} {detail.discountAmount > 0 && <p><strong>:</strong> {detail.discountAmount.toLocaleString()}</p>}
@ -139,21 +176,25 @@ const ReservationDetailView: React.FC<{
<> <>
<div className="modal-section-v2"> <div className="modal-section-v2">
<h3> </h3> <h3> </h3>
<p><strong>:</strong> {reservation.memberName}</p> <p><strong> :</strong> {reservation.themeName}</p>
<p><strong> :</strong> {formatDisplayDateTime(reservation.applicationDateTime)}</p> <p><strong> :</strong> {formatCardDateTime(reservation.date, reservation.startAt)}</p>
<p><strong> :</strong> {reservation.user.name}</p>
<p><strong> :</strong> {reservation.user.email}</p>
<p><strong> :</strong> {formatDisplayDateTime(reservation.applicationDateTime)}</p>
</div> </div>
<div className="modal-section-v2"> <div className="modal-section-v2">
<h3> </h3> <h3> </h3>
{/* <p><strong>결제금액:</strong> {reservation.payment.totalAmount.toLocaleString()}원</p> */} {/* <p><strong>결제금액:</strong> {reservation.payment.totalAmount.toLocaleString()}원</p> */}
{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.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.cancellation.cancellationRequestedAt)}</p>
<p><strong> :</strong> {formatDisplayDateTime(reservation.cancellation.cancellationCompletedAt)}</p> <p><strong> :</strong> {formatDisplayDateTime(reservation.cancellation.cancellationApprovedAt)}</p>
<p><strong> :</strong> {reservation.cancellation.cancelReason}</p> <p><strong> :</strong> {reservation.cancellation.cancelReason}</p>
<p><strong> :</strong> {reservation.cancellation.canceledBy == reservation.user.id ? '회원 본인' : '관리자'}</p>
</div> </div>
)} )}
{reservation.payment.status !== 'CANCELED' && ( {reservation.payment.status !== 'CANCELED' && (
@ -183,7 +224,7 @@ const MyReservationPageV2: React.FC = () => {
try { try {
setIsLoading(true); setIsLoading(true);
const data = await fetchMyReservationsV2(); const data = await fetchMyReservationsV2();
setReservations(data); setReservations(data.reservations);
setError(null); setError(null);
} catch (err) { } catch (err) {
setError('예약 목록을 불러오는 데 실패했습니다.'); setError('예약 목록을 불러오는 데 실패했습니다.');
@ -196,12 +237,13 @@ const MyReservationPageV2: React.FC = () => {
loadReservations(); loadReservations();
}, []); }, []);
const handleShowDetail = async (id: number) => { const handleShowDetail = async (id: string) => {
try { try {
setIsDetailLoading(true); setIsDetailLoading(true);
setDetailError(null); setDetailError(null);
setModalView('detail'); setModalView('detail');
const detailData = await fetchReservationDetailV2(id); const detailData = await fetchReservationDetailV2(id);
console.log('상세 정보:', detailData);
setSelectedReservation(detailData); setSelectedReservation(detailData);
setIsModalOpen(true); setIsModalOpen(true);
} catch (err) { } catch (err) {
@ -227,7 +269,7 @@ const MyReservationPageV2: React.FC = () => {
setIsCancelling(true); setIsCancelling(true);
setDetailError(null); setDetailError(null);
await cancelReservationV2(selectedReservation.id, reason); await cancelReservationV2(selectedReservation.id, reason);
alert('예약이 성공적으로 취소되었습니다.'); alert('예약을 취소했어요. 결제 취소까지는 3-5일 정도 소요될 수 있어요.');
handleCloseModal(); handleCloseModal();
loadReservations(); // Refresh the list loadReservations(); // Refresh the list
} catch (err) { } catch (err) {
@ -247,10 +289,11 @@ 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.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.time)}</p> <p className="summary-datetime-v2">{formatCardDateTime(res.date, res.startAt)}</p>
</div> </div>
<button <button
onClick={() => handleShowDetail(res.id)} onClick={() => handleShowDetail(res.id)}

View File

@ -30,7 +30,7 @@ const ReservationSuccessPage: React.FC = () => {
<div className="info-item"><strong>:</strong> <span>{startAt}</span></div> <div className="info-item"><strong>:</strong> <span>{startAt}</span></div>
</div> </div>
<div className="success-page-actions"> <div className="success-page-actions">
<Link to="/my-reservation" className="btn btn-secondary"> <Link to="/my-reservation/v2" className="btn btn-secondary">
</Link> </Link>
<Link to="/" className="btn btn-secondary"> <Link to="/" className="btn btn-secondary">