generated from pricelees/issue-pr-template
[#35] 결제 스키마 재정의 & 예약 조회 페이지 개선 #36
@ -2,14 +2,16 @@ import apiClient from "@_api/apiClient";
|
||||
import type {
|
||||
AdminReservationCreateRequest,
|
||||
MyReservationRetrieveListResponse,
|
||||
ReservationPaymentRequest,
|
||||
ReservationPaymentResponse,
|
||||
ReservationCreateRequest,
|
||||
ReservationCreateResponse,
|
||||
ReservationCreateWithPaymentRequest,
|
||||
ReservationDetailV2,
|
||||
ReservationPaymentRequest,
|
||||
ReservationPaymentResponse,
|
||||
ReservationRetrieveListResponse,
|
||||
ReservationRetrieveResponse,
|
||||
ReservationSearchQuery,
|
||||
ReservationSummaryListV2,
|
||||
WaitingCreateRequest
|
||||
} from "./reservationTypes";
|
||||
|
||||
@ -82,3 +84,18 @@ export const createPendingReservation = async (data: ReservationCreateRequest):
|
||||
export const confirmReservationPayment = async (id: string, data: ReservationPaymentRequest): Promise<ReservationPaymentResponse> => {
|
||||
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);
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
};
|
||||
@ -134,27 +134,38 @@ export interface ReservationPaymentResponse {
|
||||
}
|
||||
|
||||
export interface ReservationSummaryV2 {
|
||||
id: number;
|
||||
id: string;
|
||||
themeName: string;
|
||||
date: string;
|
||||
time: string;
|
||||
status: 'CONFIRMED' | 'CANCELLED';
|
||||
startAt: string;
|
||||
status: string; // 'CONFIRMED', 'CANCELED_BY_USER', etc.
|
||||
}
|
||||
|
||||
export interface ReservationSummaryListV2 {
|
||||
reservations: ReservationSummaryV2[];
|
||||
}
|
||||
|
||||
export interface ReservationDetailV2 {
|
||||
id: number;
|
||||
memberName: string;
|
||||
memberEmail: string;
|
||||
applicationDateTime: string; // yyyy년 MM월 dd일 HH시 mm분
|
||||
id: string;
|
||||
user: UserDetailV2;
|
||||
themeName: string;
|
||||
date: string;
|
||||
startAt: string;
|
||||
applicationDateTime: string;
|
||||
payment: PaymentV2;
|
||||
cancellation: CancellationV2 | null;
|
||||
}
|
||||
|
||||
export interface UserDetailV2 {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface PaymentV2 {
|
||||
paymentKey: string;
|
||||
orderId: string;
|
||||
totalAmount: number;
|
||||
method: 'CARD' | 'BANK_TRANSFER' | 'EASYPAY_PREPAID';
|
||||
method: string;
|
||||
status: 'DONE' | 'CANCELED';
|
||||
requestedAt: string;
|
||||
approvedAt: string;
|
||||
@ -167,28 +178,30 @@ export interface CardPaymentDetailV2 {
|
||||
cardType: 'CREDIT' | 'CHECK' | 'GIFT';
|
||||
ownerType: 'PERSONAL' | 'CORPORATE';
|
||||
cardNumber: string;
|
||||
amount: number;
|
||||
approvalNumber: string;
|
||||
installmentPlanMonths: number;
|
||||
isInterestFree: boolean;
|
||||
easypayProviderCode?: string;
|
||||
easypayProviderName?: string;
|
||||
easypayDiscountAmount?: number;
|
||||
}
|
||||
|
||||
export interface BankTransferPaymentDetailV2 {
|
||||
type: 'BANK_TRANSFER';
|
||||
bankCode: string;
|
||||
bankName: string;
|
||||
settlementStatus: string;
|
||||
}
|
||||
|
||||
export interface EasyPayPrepaidPaymentDetailV2 {
|
||||
type: 'EASYPAY_PREPAID';
|
||||
easypayProviderCode: string;
|
||||
providerName: string;
|
||||
amount: number;
|
||||
discountAmount: number;
|
||||
}
|
||||
|
||||
export interface CancellationV2 {
|
||||
cancellationRequestedAt: string; // ISO 8601 format
|
||||
cancellationCompletedAt: string; // ISO 8601 format
|
||||
cancellationApprovedAt: string; // ISO 8601 format
|
||||
cancelReason: string;
|
||||
canceledBy: string;
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ const Navbar: React.FC = () => {
|
||||
<div className="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul className="navbar-nav ms-auto">
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/reservation">Reservation</Link>
|
||||
<Link className="nav-link" to="/v2/reservation">Reservation</Link>
|
||||
</li>
|
||||
{!loggedIn ? (
|
||||
<li className="nav-item">
|
||||
@ -40,7 +40,7 @@ const Navbar: React.FC = () => {
|
||||
<span id="profile-name">{userName}</span>
|
||||
</a>
|
||||
<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><a className="dropdown-item" href="#" onClick={handleLogout}>Logout</a></li>
|
||||
</ul>
|
||||
|
||||
@ -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;
|
||||
@ -69,14 +69,14 @@
|
||||
}
|
||||
|
||||
/* Canceled Card Style */
|
||||
.reservation-summary-card-v2.status-cancelled {
|
||||
.reservation-summary-card-v2.status-canceled_by_user {
|
||||
background-color: #f8f9fa;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.reservation-summary-card-v2.status-cancelled .summary-theme-name-v2,
|
||||
.reservation-summary-card-v2.status-cancelled .summary-datetime-v2,
|
||||
.reservation-summary-card-v2.status-cancelled .summary-details-v2 strong {
|
||||
.reservation-summary-card-v2.status-canceled_by_user .summary-theme-name-v2,
|
||||
.reservation-summary-card-v2.status-canceled_by_user .summary-datetime-v2,
|
||||
.reservation-summary-card-v2.status-canceled_by_user .summary-details-v2 strong {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
|
||||
@ -57,8 +57,8 @@ const AdminReservationPage: React.FC = () => {
|
||||
const applyFilter = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const params = {
|
||||
memberId: filter.memberId ? Number(filter.memberId) : undefined,
|
||||
themeId: filter.themeId ? Number(filter.themeId) : undefined,
|
||||
memberId: filter.memberId ? filter.memberId : undefined,
|
||||
themeId: filter.themeId ? filter.themeId : undefined,
|
||||
dateFrom: filter.dateFrom,
|
||||
dateTo: filter.dateTo,
|
||||
};
|
||||
@ -76,10 +76,10 @@ const AdminReservationPage: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
const request = {
|
||||
memberId: Number(newReservation.memberId),
|
||||
themeId: Number(newReservation.themeId),
|
||||
memberId: newReservation.memberId,
|
||||
themeId: newReservation.themeId,
|
||||
date: newReservation.date,
|
||||
timeId: Number(newReservation.timeId),
|
||||
timeId: newReservation.timeId,
|
||||
};
|
||||
await createReservationByAdmin(request)
|
||||
.then(() => {
|
||||
|
||||
@ -1,14 +1,39 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import '../../css/my-reservation-v2.css';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
cancelReservationV2,
|
||||
fetchMyReservationsV2,
|
||||
fetchReservationDetailV2,
|
||||
cancelReservationV2
|
||||
} from '../../api/reservation/reservationAPIV2';
|
||||
import type { ReservationSummaryV2, ReservationDetailV2, PaymentV2 } from '../../api/reservation/reservationTypes';
|
||||
fetchReservationDetailV2
|
||||
} from '../../api/reservation/reservationAPI';
|
||||
import type { PaymentV2, ReservationDetailV2, ReservationSummaryV2 } 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 = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
@ -16,6 +41,7 @@ const formatDisplayDateTime = (isoString: string): string => {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true,
|
||||
second: 'numeric'
|
||||
};
|
||||
return new Intl.DateTimeFormat('ko-KR', options).format(date);
|
||||
};
|
||||
@ -71,7 +97,7 @@ const CancellationView: React.FC<{
|
||||
<div className="cancellation-view-v2">
|
||||
<h3>취소 정보 확인</h3>
|
||||
<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> {reservation.payment.totalAmount.toLocaleString()}원</p>
|
||||
</div>
|
||||
@ -106,11 +132,22 @@ const ReservationDetailView: React.FC<{
|
||||
case 'CARD':
|
||||
return (
|
||||
<>
|
||||
<p><strong>주문 ID:</strong> {payment.orderId}</p>
|
||||
{payment.totalAmount === detail.amount ? (
|
||||
<p><strong>결제 금액:</strong> {payment.totalAmount.toLocaleString()}원</p>
|
||||
<p><strong>결제 수단:</strong> {detail.easypayProviderCode ? `간편결제 (${detail.easypayProviderCode})` : '카드'}</p>
|
||||
<p><strong>카드 정보:</strong> {detail.issuerCode}({detail.ownerType}) / {detail.cardType}</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.installmentPlanMonths === 0 ? '일시불' : `${detail.installmentPlanMonths}개월`}</p>
|
||||
<p><strong>할부 방식:</strong> {detail.installmentPlanMonths === 0 ? '일시불' : `${detail.installmentPlanMonths}개월`}</p>
|
||||
<p><strong>승인 번호:</strong> {detail.approvalNumber}</p>
|
||||
</>
|
||||
);
|
||||
@ -118,13 +155,13 @@ const ReservationDetailView: React.FC<{
|
||||
return (
|
||||
<>
|
||||
<p><strong>결제 수단:</strong> 계좌이체</p>
|
||||
<p><strong>은행:</strong> {detail.bankCode}</p>
|
||||
<p><strong>은행:</strong> {detail.bankName}</p>
|
||||
</>
|
||||
);
|
||||
case 'EASYPAY_PREPAID':
|
||||
return (
|
||||
<>
|
||||
<p><strong>결제 수단:</strong> 간편결제 / {detail.easypayProviderCode}</p>
|
||||
<p><strong>결제 수단:</strong> 간편결제 / {detail.providerName}</p>
|
||||
<p><strong>총 금액 :</strong> {payment.totalAmount.toLocaleString()}원</p>
|
||||
<p><strong>결제 금액:</strong> {detail.amount.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">
|
||||
<h3>예약 정보</h3>
|
||||
<p><strong>예약자명:</strong> {reservation.memberName}</p>
|
||||
<p><strong>신청 일시:</strong> {formatDisplayDateTime(reservation.applicationDateTime)}</p>
|
||||
<p><strong>예약 테마:</strong> {reservation.themeName}</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 className="modal-section-v2">
|
||||
<h3>결제 정보</h3>
|
||||
{/* <p><strong>결제금액:</strong> {reservation.payment.totalAmount.toLocaleString()}원</p> */}
|
||||
{renderPaymentDetails(reservation.payment)}
|
||||
<p><strong>승인일시:</strong> {formatDisplayDateTime(reservation.payment.approvedAt)}</p>
|
||||
<p><strong>결제 승인 일시:</strong> {formatDisplayDateTime(reservation.payment.approvedAt)}</p>
|
||||
</div>
|
||||
{reservation.cancellation && (
|
||||
<div className="modal-section-v2 cancellation-section-v2">
|
||||
<h3>취소 정보</h3>
|
||||
<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.canceledBy == reservation.user.id ? '회원 본인' : '관리자'}</p>
|
||||
</div>
|
||||
)}
|
||||
{reservation.payment.status !== 'CANCELED' && (
|
||||
@ -183,7 +224,7 @@ const MyReservationPageV2: React.FC = () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await fetchMyReservationsV2();
|
||||
setReservations(data);
|
||||
setReservations(data.reservations);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError('예약 목록을 불러오는 데 실패했습니다.');
|
||||
@ -196,12 +237,13 @@ const MyReservationPageV2: React.FC = () => {
|
||||
loadReservations();
|
||||
}, []);
|
||||
|
||||
const handleShowDetail = async (id: number) => {
|
||||
const handleShowDetail = async (id: string) => {
|
||||
try {
|
||||
setIsDetailLoading(true);
|
||||
setDetailError(null);
|
||||
setModalView('detail');
|
||||
const detailData = await fetchReservationDetailV2(id);
|
||||
console.log('상세 정보:', detailData);
|
||||
setSelectedReservation(detailData);
|
||||
setIsModalOpen(true);
|
||||
} catch (err) {
|
||||
@ -227,7 +269,7 @@ const MyReservationPageV2: React.FC = () => {
|
||||
setIsCancelling(true);
|
||||
setDetailError(null);
|
||||
await cancelReservationV2(selectedReservation.id, reason);
|
||||
alert('예약이 성공적으로 취소되었습니다.');
|
||||
alert('예약을 취소했어요. 결제 취소까지는 3-5일 정도 소요될 수 있어요.');
|
||||
handleCloseModal();
|
||||
loadReservations(); // Refresh the list
|
||||
} catch (err) {
|
||||
@ -247,10 +289,11 @@ const MyReservationPageV2: React.FC = () => {
|
||||
{!isLoading && !error && (
|
||||
<div className="reservation-list-v2">
|
||||
{reservations.map((res) => (
|
||||
console.log(res),
|
||||
<div key={res.id} className={`reservation-summary-card-v2 status-${res.status.toLowerCase()}`}>
|
||||
<div className="summary-details-v2">
|
||||
<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>
|
||||
<button
|
||||
onClick={() => handleShowDetail(res.id)}
|
||||
|
||||
@ -30,7 +30,7 @@ const ReservationSuccessPage: React.FC = () => {
|
||||
<div className="info-item"><strong>시간:</strong> <span>{startAt}</span></div>
|
||||
</div>
|
||||
<div className="success-page-actions">
|
||||
<Link to="/my-reservation" className="btn btn-secondary">
|
||||
<Link to="/my-reservation/v2" className="btn btn-secondary">
|
||||
내 예약 목록
|
||||
</Link>
|
||||
<Link to="/" className="btn btn-secondary">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user