From 216bde2d255e1c26fecd6a0f17e28650f7de20b0 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 9 Oct 2025 15:56:38 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EB=B0=B1=EC=97=94=EB=93=9C?= =?UTF-8?q?=EC=97=90=EC=84=9C=EC=9D=98=20DTO=20=EC=8A=A4=ED=8E=99=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B2=B0=EC=A0=9C=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B3=80=EA=B2=BD=20=EC=82=AC=ED=95=AD=20=ED=94=84?= =?UTF-8?q?=EB=A1=A0=ED=8A=B8=EC=97=94=EB=93=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/payment/PaymentTypes.ts | 1 - frontend/src/api/schedule/scheduleTypes.ts | 37 ++++++++--- frontend/src/pages/ReservationStep1Page.tsx | 58 ++++++++--------- frontend/src/pages/ReservationStep2Page.tsx | 71 ++++++++++++++------- 4 files changed, 104 insertions(+), 63 deletions(-) diff --git a/frontend/src/api/payment/PaymentTypes.ts b/frontend/src/api/payment/PaymentTypes.ts index c35958ba..acd34728 100644 --- a/frontend/src/api/payment/PaymentTypes.ts +++ b/frontend/src/api/payment/PaymentTypes.ts @@ -2,7 +2,6 @@ export interface PaymentConfirmRequest { paymentKey: string; orderId: string; amount: number; - paymentType: PaymentType; } export interface PaymentCancelRequest { diff --git a/frontend/src/api/schedule/scheduleTypes.ts b/frontend/src/api/schedule/scheduleTypes.ts index 0acf9f64..b3c3042d 100644 --- a/frontend/src/api/schedule/scheduleTypes.ts +++ b/frontend/src/api/schedule/scheduleTypes.ts @@ -1,5 +1,3 @@ -import type { Difficulty } from '@_api/theme/themeTypes'; - export type ScheduleStatus = 'AVAILABLE' | 'HOLD' | 'RESERVED' | 'BLOCKED'; export const ScheduleStatus = { @@ -40,16 +38,35 @@ export interface AdminScheduleSummaryListResponse { } // 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 { - id: string, - startFrom: string, - endAt: string, - themeId: string, - themeName: string, - themeDifficulty: Difficulty, - status: ScheduleStatus + schedule: ScheduleResponse, + theme: ScheduleThemeInfo } export interface ScheduleWithThemeListResponse { schedules: ScheduleWithThemeResponse[]; -} \ No newline at end of file +} diff --git a/frontend/src/pages/ReservationStep1Page.tsx b/frontend/src/pages/ReservationStep1Page.tsx index 9ed6fd7e..fcaa4018 100644 --- a/frontend/src/pages/ReservationStep1Page.tsx +++ b/frontend/src/pages/ReservationStep1Page.tsx @@ -1,17 +1,17 @@ -import {isLoginRequiredError} from '@_api/apiClient'; -import {fetchSidoList, fetchSigunguList} from '@_api/region/regionAPI'; -import {type SidoResponse, type SigunguResponse} from '@_api/region/regionTypes'; -import {fetchSchedules, holdSchedule} from '@_api/schedule/scheduleAPI'; -import {ScheduleStatus, type ScheduleWithThemeResponse} from '@_api/schedule/scheduleTypes'; -import {getStores} from '@_api/store/storeAPI'; -import {type SimpleStoreResponse} from '@_api/store/storeTypes'; -import {fetchThemeById} from '@_api/theme/themeAPI'; -import {DifficultyKoreanMap, type ThemeInfoResponse} from '@_api/theme/themeTypes'; +import { isLoginRequiredError } from '@_api/apiClient'; +import { fetchSidoList, fetchSigunguList } from '@_api/region/regionAPI'; +import { type SidoResponse, type SigunguResponse } from '@_api/region/regionTypes'; +import { type ReservationData } from '@_api/reservation/reservationTypes'; +import { fetchSchedules, holdSchedule } from '@_api/schedule/scheduleAPI'; +import { ScheduleStatus, type ScheduleWithThemeResponse } from '@_api/schedule/scheduleTypes'; +import { getStores } from '@_api/store/storeAPI'; +import { type SimpleStoreResponse } from '@_api/store/storeTypes'; +import { fetchThemeById } from '@_api/theme/themeAPI'; +import { DifficultyKoreanMap, type ThemeInfoResponse } from '@_api/theme/themeTypes'; import '@_css/reservation-v2-1.css'; -import React, {useEffect, useState} from 'react'; -import {useLocation, useNavigate} from 'react-router-dom'; -import {type ReservationData} from '@_api/reservation/reservationTypes'; -import {formatDate} from 'src/util/DateTimeFormatter'; +import React, { useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { formatDate } from 'src/util/DateTimeFormatter'; const ReservationStep1Page: React.FC = () => { const [selectedDate, setSelectedDate] = useState(new Date()); @@ -76,7 +76,7 @@ const ReservationStep1Page: React.FC = () => { fetchSchedules(selectedStore.id, dateStr) .then(res => { const grouped = res.schedules.reduce((acc, schedule) => { - const key = schedule.themeName; + const key = schedule.theme.name; if (!acc[key]) acc[key] = []; acc[key].push(schedule); return acc; @@ -111,11 +111,11 @@ const ReservationStep1Page: React.FC = () => { const handleConfirmReservation = () => { if (!selectedSchedule) return; - holdSchedule(selectedSchedule.id) + holdSchedule(selectedSchedule.schedule.id) .then(() => { - fetchThemeById(selectedSchedule.themeId).then(res => { + fetchThemeById(selectedSchedule.theme.id).then(res => { const reservationData: ReservationData = { - scheduleId: selectedSchedule.id, + scheduleId: selectedSchedule.schedule.id, store: { id: selectedStore!.id, name: selectedStore!.name, @@ -128,8 +128,8 @@ const ReservationStep1Page: React.FC = () => { maxParticipants: res.maxParticipants, }, date: selectedDate.toLocaleDateString('en-CA'), - startFrom: selectedSchedule.startFrom, - endAt: selectedSchedule.endAt, + startFrom: selectedSchedule.schedule.startFrom, + endAt: selectedSchedule.schedule.endAt, }; navigate('/reservation/form', {state: reservationData}); }).catch(handleError); @@ -248,23 +248,23 @@ const ReservationStep1Page: React.FC = () => {

3. 시간 선택

{Object.keys(schedulesByTheme).length > 0 ? ( - Object.entries(schedulesByTheme).map(([themeName, schedules]) => ( + Object.entries(schedulesByTheme).map(([themeName, scheduleAndTheme]) => (

{themeName}

-
- {schedules.map(schedule => ( + {scheduleAndTheme.map(schedule => (
schedule.status === ScheduleStatus.AVAILABLE && setSelectedSchedule(schedule)} + key={schedule.schedule.id} + className={`time-slot ${selectedSchedule?.schedule.id === schedule.schedule.id ? 'active' : ''} ${schedule.schedule.status !== ScheduleStatus.AVAILABLE ? 'disabled' : ''}`} + onClick={() => schedule.schedule.status === ScheduleStatus.AVAILABLE && setSelectedSchedule(schedule)} > - {`${schedule.startFrom} ~ ${schedule.endAt}`} - {getStatusText(schedule.status)} + {`${schedule.schedule.startFrom} ~ ${schedule.schedule.endAt}`} + {getStatusText(schedule.schedule.status)}
))}
@@ -313,8 +313,8 @@ const ReservationStep1Page: React.FC = () => {

날짜:{formatDate(selectedDate.toLocaleDateString('ko-KR'))}

매장:{selectedStore?.name}

-

테마:{selectedSchedule.themeName}

-

시간:{`${selectedSchedule.startFrom} ~ ${selectedSchedule.endAt}`}

+

테마:{selectedSchedule.theme.name}

+

시간:{`${selectedSchedule.schedule.startFrom} ~ ${selectedSchedule.schedule.endAt}`}

diff --git a/frontend/src/pages/ReservationStep2Page.tsx b/frontend/src/pages/ReservationStep2Page.tsx index 6a6ad964..38bf1e49 100644 --- a/frontend/src/pages/ReservationStep2Page.tsx +++ b/frontend/src/pages/ReservationStep2Page.tsx @@ -1,8 +1,9 @@ -import { isLoginRequiredError } from '@_api/apiClient'; -import { confirmPayment } from '@_api/payment/paymentAPI'; -import { type PaymentConfirmRequest, PaymentType } from '@_api/payment/PaymentTypes'; +import { confirm } from '@_api/order/orderAPI'; +import type { BookingErrorResponse } from '@_api/order/orderTypes'; +import { type PaymentConfirmRequest } from '@_api/payment/PaymentTypes'; import { confirmReservation } from '@_api/reservation/reservationAPI'; import '@_css/reservation-v2-1.css'; +import type { AxiosError } from 'axios'; import React, { useEffect, useRef } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; 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 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('잘못된 접근입니다.'); @@ -66,7 +56,7 @@ const ReservationStep2Page: React.FC = () => { const generateRandomString = () => crypto.randomUUID().replace(/-/g, ''); - + paymentWidgetRef.current.requestPayment({ orderId: generateRandomString(), @@ -77,13 +67,8 @@ const ReservationStep2Page: React.FC = () => { paymentKey: data.paymentKey, orderId: data.orderId, amount: totalPrice, - paymentType: data.paymentType || PaymentType.NORMAL, }; - - confirmPayment(reservationId, paymentData) - .then(() => { - return confirmReservation(reservationId); - }) + confirm(reservationId, paymentData) .then(() => { alert('결제가 완료되었어요!'); navigate('/reservation/success', { @@ -97,10 +82,50 @@ const ReservationStep2Page: React.FC = () => { } }); }) - .catch(handleError); + .catch(err => { + const error = err as AxiosError; + 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) => { console.error("Payment request error:", error); - alert("결제 요청 중 오류가 발생했습니다."); + alert("결제 요청 중 오류가 발생했어요. 새로고침 후 다시 시도해주세요."); }); };