generated from pricelees/issue-pr-template
refactor: 백엔드에서의 DTO 스펙 및 결제 처리 로직 변경 사항 프론트엔드 반영
This commit is contained in:
parent
97be1b8a1f
commit
216bde2d25
@ -2,7 +2,6 @@ export interface PaymentConfirmRequest {
|
|||||||
paymentKey: string;
|
paymentKey: string;
|
||||||
orderId: string;
|
orderId: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
paymentType: PaymentType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaymentCancelRequest {
|
export interface PaymentCancelRequest {
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import type { Difficulty } from '@_api/theme/themeTypes';
|
|
||||||
|
|
||||||
export type ScheduleStatus = 'AVAILABLE' | 'HOLD' | 'RESERVED' | 'BLOCKED';
|
export type ScheduleStatus = 'AVAILABLE' | 'HOLD' | 'RESERVED' | 'BLOCKED';
|
||||||
|
|
||||||
export const ScheduleStatus = {
|
export const ScheduleStatus = {
|
||||||
@ -40,14 +38,33 @@ export interface AdminScheduleSummaryListResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Public
|
// Public
|
||||||
|
export interface ScheduleResponse {
|
||||||
|
id: string;
|
||||||
|
date: string;
|
||||||
|
startFrom: string;
|
||||||
|
endAt: string;
|
||||||
|
status: ScheduleStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduleThemeInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduleStoreInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduleWithStoreAndThemeResponse {
|
||||||
|
schedule: ScheduleResponse,
|
||||||
|
theme: ScheduleThemeInfo,
|
||||||
|
store: ScheduleStoreInfo,
|
||||||
|
}
|
||||||
|
|
||||||
export interface ScheduleWithThemeResponse {
|
export interface ScheduleWithThemeResponse {
|
||||||
id: string,
|
schedule: ScheduleResponse,
|
||||||
startFrom: string,
|
theme: ScheduleThemeInfo
|
||||||
endAt: string,
|
|
||||||
themeId: string,
|
|
||||||
themeName: string,
|
|
||||||
themeDifficulty: Difficulty,
|
|
||||||
status: ScheduleStatus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScheduleWithThemeListResponse {
|
export interface ScheduleWithThemeListResponse {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
import { isLoginRequiredError } from '@_api/apiClient';
|
||||||
import { fetchSidoList, fetchSigunguList } from '@_api/region/regionAPI';
|
import { fetchSidoList, fetchSigunguList } from '@_api/region/regionAPI';
|
||||||
import { type SidoResponse, type SigunguResponse } from '@_api/region/regionTypes';
|
import { type SidoResponse, type SigunguResponse } from '@_api/region/regionTypes';
|
||||||
|
import { type ReservationData } from '@_api/reservation/reservationTypes';
|
||||||
import { fetchSchedules, holdSchedule } from '@_api/schedule/scheduleAPI';
|
import { fetchSchedules, holdSchedule } from '@_api/schedule/scheduleAPI';
|
||||||
import { ScheduleStatus, type ScheduleWithThemeResponse } from '@_api/schedule/scheduleTypes';
|
import { ScheduleStatus, type ScheduleWithThemeResponse } from '@_api/schedule/scheduleTypes';
|
||||||
import { getStores } from '@_api/store/storeAPI';
|
import { getStores } from '@_api/store/storeAPI';
|
||||||
@ -10,7 +11,6 @@ import {DifficultyKoreanMap, type ThemeInfoResponse} from '@_api/theme/themeType
|
|||||||
import '@_css/reservation-v2-1.css';
|
import '@_css/reservation-v2-1.css';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import {type ReservationData} from '@_api/reservation/reservationTypes';
|
|
||||||
import { formatDate } from 'src/util/DateTimeFormatter';
|
import { formatDate } from 'src/util/DateTimeFormatter';
|
||||||
|
|
||||||
const ReservationStep1Page: React.FC = () => {
|
const ReservationStep1Page: React.FC = () => {
|
||||||
@ -76,7 +76,7 @@ const ReservationStep1Page: React.FC = () => {
|
|||||||
fetchSchedules(selectedStore.id, dateStr)
|
fetchSchedules(selectedStore.id, dateStr)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const grouped = res.schedules.reduce((acc, schedule) => {
|
const grouped = res.schedules.reduce((acc, schedule) => {
|
||||||
const key = schedule.themeName;
|
const key = schedule.theme.name;
|
||||||
if (!acc[key]) acc[key] = [];
|
if (!acc[key]) acc[key] = [];
|
||||||
acc[key].push(schedule);
|
acc[key].push(schedule);
|
||||||
return acc;
|
return acc;
|
||||||
@ -111,11 +111,11 @@ const ReservationStep1Page: React.FC = () => {
|
|||||||
const handleConfirmReservation = () => {
|
const handleConfirmReservation = () => {
|
||||||
if (!selectedSchedule) return;
|
if (!selectedSchedule) return;
|
||||||
|
|
||||||
holdSchedule(selectedSchedule.id)
|
holdSchedule(selectedSchedule.schedule.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
fetchThemeById(selectedSchedule.themeId).then(res => {
|
fetchThemeById(selectedSchedule.theme.id).then(res => {
|
||||||
const reservationData: ReservationData = {
|
const reservationData: ReservationData = {
|
||||||
scheduleId: selectedSchedule.id,
|
scheduleId: selectedSchedule.schedule.id,
|
||||||
store: {
|
store: {
|
||||||
id: selectedStore!.id,
|
id: selectedStore!.id,
|
||||||
name: selectedStore!.name,
|
name: selectedStore!.name,
|
||||||
@ -128,8 +128,8 @@ const ReservationStep1Page: React.FC = () => {
|
|||||||
maxParticipants: res.maxParticipants,
|
maxParticipants: res.maxParticipants,
|
||||||
},
|
},
|
||||||
date: selectedDate.toLocaleDateString('en-CA'),
|
date: selectedDate.toLocaleDateString('en-CA'),
|
||||||
startFrom: selectedSchedule.startFrom,
|
startFrom: selectedSchedule.schedule.startFrom,
|
||||||
endAt: selectedSchedule.endAt,
|
endAt: selectedSchedule.schedule.endAt,
|
||||||
};
|
};
|
||||||
navigate('/reservation/form', {state: reservationData});
|
navigate('/reservation/form', {state: reservationData});
|
||||||
}).catch(handleError);
|
}).catch(handleError);
|
||||||
@ -248,23 +248,23 @@ const ReservationStep1Page: React.FC = () => {
|
|||||||
<h3>3. 시간 선택</h3>
|
<h3>3. 시간 선택</h3>
|
||||||
<div className="schedule-list">
|
<div className="schedule-list">
|
||||||
{Object.keys(schedulesByTheme).length > 0 ? (
|
{Object.keys(schedulesByTheme).length > 0 ? (
|
||||||
Object.entries(schedulesByTheme).map(([themeName, schedules]) => (
|
Object.entries(schedulesByTheme).map(([themeName, scheduleAndTheme]) => (
|
||||||
<div key={themeName} className="theme-schedule-group">
|
<div key={themeName} className="theme-schedule-group">
|
||||||
<div className="theme-header">
|
<div className="theme-header">
|
||||||
<h4>{themeName}</h4>
|
<h4>{themeName}</h4>
|
||||||
<button onClick={() => openThemeModal(schedules[0].themeId)}
|
<button onClick={() => openThemeModal(scheduleAndTheme[0].theme.id)}
|
||||||
className="theme-detail-button">상세보기
|
className="theme-detail-button">상세보기
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="time-slots">
|
<div className="time-slots">
|
||||||
{schedules.map(schedule => (
|
{scheduleAndTheme.map(schedule => (
|
||||||
<div
|
<div
|
||||||
key={schedule.id}
|
key={schedule.schedule.id}
|
||||||
className={`time-slot ${selectedSchedule?.id === schedule.id ? 'active' : ''} ${schedule.status !== ScheduleStatus.AVAILABLE ? 'disabled' : ''}`}
|
className={`time-slot ${selectedSchedule?.schedule.id === schedule.schedule.id ? 'active' : ''} ${schedule.schedule.status !== ScheduleStatus.AVAILABLE ? 'disabled' : ''}`}
|
||||||
onClick={() => schedule.status === ScheduleStatus.AVAILABLE && setSelectedSchedule(schedule)}
|
onClick={() => schedule.schedule.status === ScheduleStatus.AVAILABLE && setSelectedSchedule(schedule)}
|
||||||
>
|
>
|
||||||
{`${schedule.startFrom} ~ ${schedule.endAt}`}
|
{`${schedule.schedule.startFrom} ~ ${schedule.schedule.endAt}`}
|
||||||
<span className="time-availability">{getStatusText(schedule.status)}</span>
|
<span className="time-availability">{getStatusText(schedule.schedule.status)}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -313,8 +313,8 @@ const ReservationStep1Page: React.FC = () => {
|
|||||||
<div className="modal-section modal-info-grid">
|
<div className="modal-section modal-info-grid">
|
||||||
<p><strong>날짜:</strong><span>{formatDate(selectedDate.toLocaleDateString('ko-KR'))}</span></p>
|
<p><strong>날짜:</strong><span>{formatDate(selectedDate.toLocaleDateString('ko-KR'))}</span></p>
|
||||||
<p><strong>매장:</strong><span>{selectedStore?.name}</span></p>
|
<p><strong>매장:</strong><span>{selectedStore?.name}</span></p>
|
||||||
<p><strong>테마:</strong><span>{selectedSchedule.themeName}</span></p>
|
<p><strong>테마:</strong><span>{selectedSchedule.theme.name}</span></p>
|
||||||
<p><strong>시간:</strong><span>{`${selectedSchedule.startFrom} ~ ${selectedSchedule.endAt}`}</span></p>
|
<p><strong>시간:</strong><span>{`${selectedSchedule.schedule.startFrom} ~ ${selectedSchedule.schedule.endAt}`}</span></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>
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
import { confirm } from '@_api/order/orderAPI';
|
||||||
import { confirmPayment } from '@_api/payment/paymentAPI';
|
import type { BookingErrorResponse } from '@_api/order/orderTypes';
|
||||||
import { type PaymentConfirmRequest, PaymentType } from '@_api/payment/PaymentTypes';
|
import { type PaymentConfirmRequest } from '@_api/payment/PaymentTypes';
|
||||||
import { confirmReservation } from '@_api/reservation/reservationAPI';
|
import { confirmReservation } from '@_api/reservation/reservationAPI';
|
||||||
import '@_css/reservation-v2-1.css';
|
import '@_css/reservation-v2-1.css';
|
||||||
|
import type { AxiosError } from 'axios';
|
||||||
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 } from 'src/util/DateTimeFormatter';
|
import { formatDate } from 'src/util/DateTimeFormatter';
|
||||||
@ -21,17 +22,6 @@ const ReservationStep2Page: React.FC = () => {
|
|||||||
|
|
||||||
const { reservationId, storeName, themeName, themePrice, totalPrice, date, time, participantCount } = location.state || {};
|
const { reservationId, storeName, themeName, themePrice, totalPrice, date, time, participantCount } = 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(() => {
|
useEffect(() => {
|
||||||
if (!reservationId) {
|
if (!reservationId) {
|
||||||
alert('잘못된 접근입니다.');
|
alert('잘못된 접근입니다.');
|
||||||
@ -77,13 +67,8 @@ const ReservationStep2Page: React.FC = () => {
|
|||||||
paymentKey: data.paymentKey,
|
paymentKey: data.paymentKey,
|
||||||
orderId: data.orderId,
|
orderId: data.orderId,
|
||||||
amount: totalPrice,
|
amount: totalPrice,
|
||||||
paymentType: data.paymentType || PaymentType.NORMAL,
|
|
||||||
};
|
};
|
||||||
|
confirm(reservationId, paymentData)
|
||||||
confirmPayment(reservationId, paymentData)
|
|
||||||
.then(() => {
|
|
||||||
return confirmReservation(reservationId);
|
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
alert('결제가 완료되었어요!');
|
alert('결제가 완료되었어요!');
|
||||||
navigate('/reservation/success', {
|
navigate('/reservation/success', {
|
||||||
@ -97,10 +82,50 @@ const ReservationStep2Page: React.FC = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(handleError);
|
.catch(err => {
|
||||||
|
const error = err as AxiosError<BookingErrorResponse>;
|
||||||
|
const errorCode = error.response?.data?.code;
|
||||||
|
const errorMessage = error.response?.data?.message;
|
||||||
|
|
||||||
|
if (errorCode === 'B000') {
|
||||||
|
alert(`예약을 완료할 수 없어요.(${errorMessage})`);
|
||||||
|
navigate('/reservation');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trial = error.response?.data?.trial || 0;
|
||||||
|
if (trial < 2) {
|
||||||
|
alert(errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert(errorMessage);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const agreeToOnsitePayment = window.confirm('재시도 횟수를 초과했어요. 현장결제를 하시겠어요?');
|
||||||
|
|
||||||
|
if (agreeToOnsitePayment) {
|
||||||
|
confirmReservation(reservationId)
|
||||||
|
.then(() => {
|
||||||
|
navigate('/reservation/success', {
|
||||||
|
state: {
|
||||||
|
storeName,
|
||||||
|
themeName,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
participantCount,
|
||||||
|
totalPrice,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert('다음에 다시 시도해주세요. 메인 페이지로 이동할게요.');
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
}).catch((error: any) => {
|
}).catch((error: any) => {
|
||||||
console.error("Payment request error:", error);
|
console.error("Payment request error:", error);
|
||||||
alert("결제 요청 중 오류가 발생했습니다.");
|
alert("결제 요청 중 오류가 발생했어요. 새로고침 후 다시 시도해주세요.");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user