roomescape-refactored/frontend/src/pages/v2/ReservationStep2PageV21.tsx
pricelees 675a5b8854 [#41] 예약 스키마 재정의 (#42)
<!-- 제목 양식 -->
<!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) -->

## 📝 관련 이슈 및 PR

**PR과 관련된 이슈 번호**
- #41

##  작업 내용
<!-- 어떤 작업을 했는지 알려주세요! -->
- 예약 스키마 & API 재정의
- 새로운 기능에 맞춘 프론트엔드 페이지 추가
- Controller 이후 응답(성공, 실패) 로그에 Endpoint 추가
- 전환으로 인해 미사용되는 코드 및 테스트 전체 제거

## 🧪 테스트
<!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! -->
<img width="528" alt="테스트 커버리지" src="attachments/a4899c0a-2919-4993-bd3b-a71bc601137d">

- 예약 & 결제 통합 테스트 작성 완료
- 결제 테스트는 통합 테스트에서는 Client를 mocking하는 방식 + 별도의 Client 슬라이스 테스트로 진행

## 📚 참고 자료 및 기타
<!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! -->

Reviewed-on: #42
Co-authored-by: pricelees <priceelees@gmail.com>
Co-committed-by: pricelees <priceelees@gmail.com>
2025-09-09 00:43:39 +00:00

132 lines
4.8 KiB
TypeScript

import { isLoginRequiredError } from '@_api/apiClient';
import { confirmPayment } from '@_api/payment/paymentAPI';
import { PaymentType, type PaymentConfirmRequest } from '@_api/payment/PaymentTypes';
import { confirmReservation } from '@_api/reservation/reservationAPIV2';
import '@_css/reservation-v2-1.css';
import React, { useEffect, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
declare global {
interface Window {
PaymentWidget: any;
}
}
const ReservationStep2PageV21: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const paymentWidgetRef = useRef<any>(null);
const paymentMethodsRef = useRef<any>(null);
const { reservationId, themeName, date, startAt, price } = location.state || {};
const handleError = (err: any) => {
if (isLoginRequiredError(err)) {
alert('로그인이 필요해요.');
navigate('/login', { state: { from: location } });
} else {
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
alert(message);
console.error(err);
}
};
useEffect(() => {
if (!reservationId) {
alert('잘못된 접근입니다.');
navigate('/v2-1/reservation');
return;
}
const script = document.createElement('script');
script.src = 'https://js.tosspayments.com/v1/payment-widget';
script.async = true;
document.head.appendChild(script);
script.onload = () => {
const widgetClientKey = "test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm";
const paymentWidget = window.PaymentWidget(widgetClientKey, window.PaymentWidget.ANONYMOUS);
paymentWidgetRef.current = paymentWidget;
const paymentMethods = paymentWidget.renderPaymentMethods(
"#payment-method",
{ value: price },
{ variantKey: "DEFAULT" }
);
paymentMethodsRef.current = paymentMethods;
};
}, [reservationId, price, navigate]);
const handlePayment = () => {
if (!paymentWidgetRef.current || !reservationId) {
alert('결제 위젯이 로드되지 않았거나 예약 정보가 없습니다.');
return;
}
const generateRandomString = () =>
crypto.randomUUID().replace(/-/g, '');
paymentWidgetRef.current.requestPayment({
orderId: generateRandomString(),
orderName: `${themeName} 예약 결제`,
amount: price,
}).then((data: any) => {
const paymentData: PaymentConfirmRequest = {
paymentKey: data.paymentKey,
orderId: data.orderId,
amount: price, // Use the price from component state instead of widget response
paymentType: data.paymentType || PaymentType.NORMAL,
};
confirmPayment(reservationId, paymentData)
.then(() => {
return confirmReservation(reservationId);
})
.then(() => {
alert('결제가 완료되었어요!');
navigate('/v2-1/reservation/success', {
state: {
themeName,
date,
startAt,
}
});
})
.catch(handleError);
}).catch((error: any) => {
console.error("Payment request error:", error);
alert("결제 요청 중 오류가 발생했습니다.");
});
};
if (!reservationId) {
return null;
}
return (
<div className="reservation-v21-container">
<h2 className="page-title"></h2>
<div className="step-section">
<h3> </h3>
<p><strong>:</strong> {themeName}</p>
<p><strong>:</strong> {formatDate(date)}</p>
<p><strong>:</strong> {formatTime(startAt)}</p>
<p><strong>:</strong> {price.toLocaleString()}</p>
</div>
<div className="step-section">
<h3> </h3>
<div id="payment-method" className="w-100"></div>
<div id="agreement" className="w-100"></div>
</div>
<div className="next-step-button-container">
<button onClick={handlePayment} className="next-step-button">
{price.toLocaleString()}
</button>
</div>
</div>
);
};
export default ReservationStep2PageV21;