);
};
diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx
index e9472583..db4704d3 100644
--- a/frontend/src/pages/LoginPage.tsx
+++ b/frontend/src/pages/LoginPage.tsx
@@ -1,6 +1,7 @@
-import React, { useState } from 'react';
-import { useLocation, useNavigate } from 'react-router-dom';
-import { useAuth } from '../context/AuthContext';
+import React, {useState} from 'react';
+import {useLocation, useNavigate} from 'react-router-dom';
+import {useAuth} from '@_context/AuthContext';
+import '@_css/login-page-v2.css';
const LoginPage: React.FC = () => {
const [email, setEmail] = useState('');
@@ -11,36 +12,53 @@ const LoginPage: React.FC = () => {
const from = location.state?.from?.pathname || '/';
- const handleLogin = async () => {
+ const handleLogin = async (e: React.FormEvent) => {
+ e.preventDefault();
try {
- await login({email, password});
-
+ const principalType = from.startsWith('/admin') ? 'ADMIN' : 'USER';
+ await login({ account: email, password: password, principalType: principalType });
+
alert('로그인에 성공했어요!');
navigate(from, { replace: true });
} catch (error: any) {
const message = error.response?.data?.message || '로그인에 실패했어요. 이메일과 비밀번호를 확인해주세요.';
alert(message);
console.error('로그인 실패:', error);
- setEmail('');
setPassword('');
}
}
return (
-
-
Login
-
- setEmail(e.target.value)} />
-
-
- setPassword(e.target.value)} />
-
-
-
-
-
+
);
};
-export default LoginPage;
\ No newline at end of file
+export default LoginPage;
diff --git a/frontend/src/pages/MyReservationPage.tsx b/frontend/src/pages/MyReservationPage.tsx
index 96102c63..d25d07f8 100644
--- a/frontend/src/pages/MyReservationPage.tsx
+++ b/frontend/src/pages/MyReservationPage.tsx
@@ -1,88 +1,382 @@
-import React, { useEffect, useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-import { cancelWaiting, fetchMyReservations } from '@_api/reservation/reservationAPI';
-import type { MyReservationRetrieveResponse } from '@_api/reservation/reservationTypes';
-import { ReservationStatus } from '@_api/reservation/reservationTypes';
-import { isLoginRequiredError } from '@_api/apiClient';
+import {cancelPayment} from '@_api/payment/paymentAPI';
+import type {PaymentRetrieveResponse} from '@_api/payment/PaymentTypes';
+import {cancelReservation, fetchDetailById, fetchSummaryByMember} from '@_api/reservation/reservationAPI';
+import {
+ type ReservationDetail,
+ ReservationStatus,
+ type ReservationSummaryRetrieveResponse
+} from '@_api/reservation/reservationTypes';
+import React, {useEffect, useState} from 'react';
+import '@_css/my-reservation-v2.css';
+const getReservationStatus = (reservation: ReservationSummaryRetrieveResponse): { className: string, text: string } => {
+ const now = new Date();
+ const reservationDateTime = new Date(`${reservation.date}T${reservation.startAt}`);
+
+ switch (reservation.status) {
+ case ReservationStatus.CANCELED:
+ return { className: 'status-canceled', text: '취소됨' };
+ case ReservationStatus.CONFIRMED:
+ if (reservationDateTime < now) {
+ return { className: 'status-completed', text: '이용완료' };
+ }
+ return { className: 'status-confirmed', text: '예약확정' };
+ case ReservationStatus.PENDING:
+ return { className: 'status-pending', text: '입금대기' };
+ default:
+ return { className: `status-${reservation.status.toLowerCase()}`, text: reservation.status };
+ }
+};
+
+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 options: Intl.DateTimeFormatOptions = {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ hour12: true,
+ second: 'numeric'
+ };
+ return new Intl.DateTimeFormat('ko-KR', options).format(date);
+};
+
+const formatCardDateTime = (dateStr: string, timeStr: string): string => {
+ const date = new Date(`${dateStr}T${timeStr}`);
+ const currentYear = new Date().getFullYear();
+ const reservationYear = date.getFullYear();
+
+ const days = ['일', '월', '화', '수', '목', '금', '토'];
+ const dayOfWeek = days[date.getDay()];
+ const month = date.getMonth() + 1;
+ const day = date.getDate();
+ let hours = date.getHours();
+ const minutes = date.getMinutes();
+ const ampm = hours >= 12 ? '오후' : '오전';
+ hours = hours % 12;
+ hours = hours ? hours : 12;
+
+ let datePart = '';
+ if (currentYear === reservationYear) {
+ datePart = `${month}월 ${day}일(${dayOfWeek})`;
+ } else {
+ datePart = `${reservationYear}년 ${month}월 ${day}일(${dayOfWeek})`;
+ }
+
+ let timePart = `${ampm} ${hours}시`;
+ if (minutes !== 0) {
+ timePart += ` ${minutes}분`;
+ }
+
+ return `${datePart} ${timePart}`;
+};
+
+// --- Cancellation View Component ---
+const CancellationView: React.FC<{
+ reservation: ReservationDetail;
+ onCancelSubmit: (reason: string) => void;
+ onBack: () => void;
+ isCancelling: boolean;
+}> = ({ reservation, onCancelSubmit, onBack, isCancelling }) => {
+ const [reason, setReason] = useState('');
+
+ const handleSubmit = () => {
+ if (!reason.trim()) {
+ alert('취소 사유를 입력해주세요.');
+ return;
+ }
+ onCancelSubmit(reason);
+ };
+
+ return (
+
+
취소 정보 확인
+
+
테마: {reservation.themeName}
+
신청 일시: {formatDisplayDateTime(reservation.applicationDateTime)}
+ {reservation.payment &&
결제 금액: {reservation.payment.totalAmount.toLocaleString()}원
}
+
+
+ );
+};
+
+
+const ReservationDetailView: React.FC<{
+ reservation: ReservationDetail;
+ onGoToCancel: () => void;
+}> = ({ reservation, onGoToCancel }) => {
+
+ const renderPaymentSubDetails = (payment: PaymentRetrieveResponse) => {
+ if (!payment.detail) {
+ return
결제 상세 정보를 찾을 수 없어요. 관리자에게 문의해주세요.
;
+ }
+ const { detail } = payment;
+
+ switch (detail.type) {
+ case 'CARD':
+ return (
+ <>
+ {payment.totalAmount !== detail.amount && (
+ <>
+
(카드)승인 금액: {detail.amount.toLocaleString()}원
+ {detail.easypayDiscountAmount && (
+
(간편결제)할인 금액: {detail.easypayDiscountAmount.toLocaleString()}원
+ )}
+ >
+ )}
+ {detail.easypayProviderName && (
+
간편결제사: {detail.easypayProviderName}
+ )}
+
카드사 / 구분: {detail.issuerCode}({detail.ownerType}) / {detail.cardType}
+
카드 번호: {detail.cardNumber}
+
할부 방식: {detail.installmentPlanMonths === 0 ? '일시불' : `${detail.installmentPlanMonths}개월`}
+
승인 번호: {detail.approvalNumber}
+ >
+ );
+ case 'BANK_TRANSFER':
+ return (
+ <>
+
은행: {detail.bankName}
+
정산 상태: {detail.settlementStatus}
+ >
+ );
+ case 'EASYPAY_PREPAID':
+ return (
+ <>
+
결제 금액: {detail.amount.toLocaleString()}원
+ {detail.discountAmount > 0 &&
포인트 사용: {detail.discountAmount.toLocaleString()}원
}
+ >
+ );
+ default:
+ return
상세 결제 수단 정보를 표시할 수 없습니다.
;
+ }
+ };
+
+ return (
+ <>
+
+
예약 정보
+
예약 테마: {reservation.themeName}
+
이용 예정일: {formatCardDateTime(reservation.date, reservation.startAt)}
+
예약자 이름: {reservation.user.name}
+
예약자 이메일: {reservation.user.phone}
+
예약 신청 일시: {formatDisplayDateTime(reservation.applicationDateTime)}
+
+
+ {!reservation.payment ? (
+
+
결제 정보
+
결제 정보를 찾을 수 없어요.
+
+ ) : (
+ <>
+
+
결제 정보
+
주문 ID: {reservation.payment.orderId}
+
총 결제액: {reservation.payment.totalAmount.toLocaleString()}원
+
결제 수단: {reservation.payment.method}
+ {reservation.payment.approvedAt &&
결제 승인 일시: {formatDisplayDateTime(reservation.payment.approvedAt)}
}
+
+
+
결제 상세 정보
+ {renderPaymentSubDetails(reservation.payment)}
+
+ >
+ )}
+
+ {reservation.payment && reservation.payment.cancel && (
+
+
취소 정보
+
취소 요청 일시: {formatDisplayDateTime(reservation.payment.cancel.cancellationRequestedAt)}
+
환불 완료 일시: {formatDisplayDateTime(reservation.payment.cancel.cancellationApprovedAt)}
+
취소 사유: {reservation.payment.cancel.cancelReason}
+
취소 요청인: {reservation.payment.cancel.canceledBy == reservation.user.id ? '회원 본인' : '관리자'}
+
+ )}
+ {reservation.payment && reservation.payment.status !== 'CANCELED' && (
+
+
+
+ )}
+ >
+ );
+};
+
+// --- Main Page Component ---
const MyReservationPage: React.FC = () => {
- const [reservations, setReservations] = useState
([]);
- const navigate = useNavigate();
+ const [reservations, setReservations] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
- 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);
+ const [selectedReservation, setSelectedReservation] = useState(null);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [isDetailLoading, setIsDetailLoading] = useState(false);
+ const [detailError, setDetailError] = useState(null);
+
+ const [modalView, setModalView] = useState<'detail' | 'cancel'>('detail');
+ const [isCancelling, setIsCancelling] = useState(false);
+
+ const loadReservations = async () => {
+ try {
+ setIsLoading(true);
+ const data = await fetchSummaryByMember();
+ setReservations(data.reservations);
+ setError(null);
+ } catch (err) {
+ setError('예약 목록을 불러오는 데 실패했습니다.');
+ } finally {
+ setIsLoading(false);
}
};
useEffect(() => {
- fetchMyReservations()
- .then(res => setReservations(res.reservations))
- .catch(handleError);
+ loadReservations();
}, []);
- const _cancelWaiting = (id: string) => {
- cancelWaiting(id)
- .then(() => {
- alert('예약 대기가 취소되었습니다.');
- setReservations(reservations.filter(r => r.id.toString() !== id));
- })
- .catch(handleError);
+ const handleShowDetail = async (id: string, themeName: string, date: string, time: string) => {
+ try {
+ setIsDetailLoading(true);
+ setDetailError(null);
+ setModalView('detail');
+ const detailData = await fetchDetailById(id);
+ setSelectedReservation({
+ id: detailData.id,
+ themeName: themeName,
+ date: date,
+ startAt: time,
+ user: detailData.user,
+ applicationDateTime: detailData.applicationDateTime,
+ payment: detailData.payment
+ });
+ setIsModalOpen(true);
+ } catch (err) {
+ setDetailError('예약 상세 정보를 불러오는 데 실패했습니다.');
+ } finally {
+ setIsDetailLoading(false);
+ }
};
- const getStatusText = (status: ReservationStatus, rank: number) => {
- if (status === ReservationStatus.CONFIRMED) {
- return '예약';
- }
- if (status === ReservationStatus.CONFIRMED_PAYMENT_REQUIRED) {
- return '예약 - 결제 필요';
- }
- if (status === ReservationStatus.WAITING) {
- return `${rank}번째 예약 대기`;
- }
- return '';
+ const handleCloseModal = () => {
+ setIsModalOpen(false);
+ setSelectedReservation(null);
};
+ const handleCancelSubmit = async (reason: string) => {
+ if (!selectedReservation) return;
+
+ if (!window.confirm('정말 취소하시겠어요?')) {
+ return;
+ }
+
+ try {
+ setIsCancelling(true);
+ setDetailError(null);
+ await cancelReservation(selectedReservation.id, reason);
+ cancelPayment({ reservationId: selectedReservation.id, cancelReason: reason });
+ alert('예약을 취소했어요. 결제 취소까지는 3-5일 정도 소요될 수 있어요.');
+ handleCloseModal();
+ await loadReservations(); // Refresh the list
+ } catch (err) {
+ setDetailError(err instanceof Error ? err.message : '예약 취소에 실패했습니다.');
+ } finally {
+ setIsCancelling(true);
+ }
+ };
+ console.log("reservations=", reservations);
+
return (
-
-
내 예약
-
-
-
-
- | 테마 |
- 날짜 |
- 시간 |
- 상태 |
- 대기 취소 |
- paymentKey |
- 결제금액 |
- |
-
-
-
- {reservations.map(r => (
-
- | {r.themeName} |
- {r.date} |
- {r.time} |
- {getStatusText(r.status, r.rank)} |
-
- {r.status === ReservationStatus.WAITING &&
- }
- |
- {r.paymentKey} |
- {r.amount} |
- |
-
- ))}
-
-
+
+
내 예약 V2
+
+ {isLoading &&
목록 로딩 중...
}
+ {error &&
{error}
}
+
+ {!isLoading && !error && (
+
+ {reservations.map((res) => {
+ const status = getReservationStatus(res);
+ return (
+
+
{status.text}
+
+
{res.themeName}
+
{formatCardDateTime(res.date, res.startAt)}
+
+
+
+ );
+ })}
+
+ )}
+
+ {isModalOpen && selectedReservation && (
+
+
e.stopPropagation()}>
+
+
{modalView === 'detail' ? '예약 상세 정보' : '예약 취소'}
+ {detailError &&
{detailError}
}
+
+ {modalView === 'detail' ? (
+
setModalView('cancel')}
+ />
+ ) : (
+ setModalView('detail')}
+ isCancelling={isCancelling}
+ />
+ )}
+
+
+ )}
);
};
diff --git a/frontend/src/pages/v2/ReservationFormPage.tsx b/frontend/src/pages/ReservationFormPage.tsx
similarity index 68%
rename from frontend/src/pages/v2/ReservationFormPage.tsx
rename to frontend/src/pages/ReservationFormPage.tsx
index f5243b53..31a54874 100644
--- a/frontend/src/pages/v2/ReservationFormPage.tsx
+++ b/frontend/src/pages/ReservationFormPage.tsx
@@ -1,9 +1,10 @@
-import { isLoginRequiredError } from '@_api/apiClient';
-import { createPendingReservation } from '@_api/reservation/reservationAPIV2';
+import {isLoginRequiredError} from '@_api/apiClient';
+import {createPendingReservation} from '@_api/reservation/reservationAPI';
+import {fetchContact} from '@_api/user/userAPI';
import '@_css/reservation-v2-1.css';
-import React, { useState } from 'react';
-import { useLocation, useNavigate } from 'react-router-dom';
-import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
+import React, {useEffect, useState} from 'react';
+import {useLocation, useNavigate} from 'react-router-dom';
+import {formatDate, formatTime} from 'src/util/DateTimeFormatter';
const ReservationFormPage: React.FC = () => {
const navigate = useNavigate();
@@ -12,8 +13,25 @@ const ReservationFormPage: React.FC = () => {
const [reserverName, setReserverName] = useState('');
const [reserverContact, setReserverContact] = useState('');
- const [participantCount, setParticipantCount] = useState(2);
+ const [participantCount, setParticipantCount] = useState(theme.minParticipants || 1);
const [requirement, setRequirement] = useState('');
+ const [isLoadingUserInfo, setIsLoadingUserInfo] = useState(true);
+
+ useEffect(() => {
+ const fetchUserInfo = async () => {
+ try {
+ const userContact = await fetchContact();
+ setReserverName(userContact.name || '');
+ setReserverContact(userContact.phone || '');
+ } catch (err) {
+ console.warn('사용자 정보를 가져오지 못했습니다:', err);
+ } finally {
+ setIsLoadingUserInfo(false);
+ }
+ };
+
+ fetchUserInfo();
+ }, []);
const handleError = (err: any) => {
if (isLoginRequiredError(err)) {
@@ -26,10 +44,6 @@ const ReservationFormPage: React.FC = () => {
}
};
- const handleCountChange = (delta: number) => {
- setParticipantCount(prev => Math.max(theme.minParticipants, Math.min(theme.maxParticipants, prev + delta)));
- };
-
const handlePayment = () => {
if (!reserverName || !reserverContact) {
alert('예약자명과 연락처를 입력해주세요.');
@@ -46,7 +60,7 @@ const ReservationFormPage: React.FC = () => {
createPendingReservation(reservationData)
.then(res => {
- navigate('/v2-1/reservation/payment', {
+ navigate('/reservation/payment', {
state: {
reservationId: res.id,
themeName: theme.name,
@@ -60,7 +74,6 @@ const ReservationFormPage: React.FC = () => {
};
if (!scheduleId || !theme) {
- // Handle case where state is not passed correctly
return (
잘못된 접근
@@ -85,11 +98,25 @@ const ReservationFormPage: React.FC = () => {
예약자 정보
- setReserverName(e.target.value)} />
+ setReserverName(e.target.value)}
+ disabled={isLoadingUserInfo}
+ placeholder={isLoadingUserInfo ? "로딩 중..." : "예약자명을 입력하세요"}
+ />
- setReserverContact(e.target.value)} placeholder="'-' 없이 입력"/>
+ setReserverContact(e.target.value)}
+ disabled={isLoadingUserInfo}
+ placeholder={isLoadingUserInfo ? "로딩 중..." : "'-' 없이 입력"}
+ />
diff --git a/frontend/src/pages/ReservationPage.tsx b/frontend/src/pages/ReservationPage.tsx
deleted file mode 100644
index 4d8c95ec..00000000
--- a/frontend/src/pages/ReservationPage.tsx
+++ /dev/null
@@ -1,198 +0,0 @@
-import React, { useEffect, useState, useRef } from 'react';
-import { useLocation, useNavigate } from 'react-router-dom';
-import Flatpickr from 'react-flatpickr';
-import 'flatpickr/dist/flatpickr.min.css';
-import { fetchThemes } from '@_api/theme/themeAPI';
-import { fetchTimesWithAvailability } from '@_api/time/timeAPI';
-import { createReservationWithPayment, createWaiting } from '@_api/reservation/reservationAPI';
-import type { ThemeRetrieveResponse } from '@_api/theme/themeTypes';
-import type { TimeWithAvailabilityResponse } from '@_api/time/timeTypes';
-import { isLoginRequiredError } from '@_api/apiClient';
-
-declare global {
- interface Window {
- PaymentWidget: any;
- }
-}
-
-const ReservationPage: React.FC = () => {
- const [selectedDate, setSelectedDate] = useState
(new Date());
- const [themes, setThemes] = useState([]);
- const [selectedTheme, setSelectedTheme] = useState(null);
- const [times, setTimes] = useState([]);
- const [selectedTime, setSelectedTime] = useState<{ id: string, isAvailable: boolean } | null>(null);
- const paymentWidgetRef = useRef(null);
- const paymentMethodsRef = useRef(null);
- const navigate = useNavigate();
- const location = useLocation();
-
- 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(() => {
- 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: 1000 },
- { variantKey: "DEFAULT" }
- );
- paymentMethodsRef.current = paymentMethods;
- };
-
- fetchThemes().then(res => setThemes(res.themes)).catch(handleError);
- }, []);
-
- useEffect(() => {
- if (selectedDate && selectedTheme) {
- const dateStr = selectedDate.toLocaleDateString('en-CA');
- fetchTimesWithAvailability(dateStr, selectedTheme)
- .then(res => {
- setTimes(res.times);
- setSelectedTime(null);
- })
- .catch(handleError);
- }
- }, [selectedDate, selectedTheme]);
-
- const handleReservation = () => {
- if (!selectedDate || !selectedTheme || !selectedTime || !paymentWidgetRef.current) {
- alert('날짜, 테마, 시간을 모두 선택해주세요.');
- return;
- }
-
- const reservationData = {
- date: selectedDate.toLocaleDateString('en-CA'),
- themeId: selectedTheme,
- timeId: selectedTime.id,
- };
-
- const generateRandomString = () =>
- crypto.randomUUID().replace(/-/g, '');
-
- paymentWidgetRef.current.requestPayment({
- orderId: generateRandomString(),
- orderName: "테스트 방탈출 예약 결제 1건",
- amount: 1000,
- }).then(function (data: any) {
- const reservationPaymentRequest = {
- ...reservationData,
- paymentKey: data.paymentKey,
- orderId: data.orderId,
- amount: data.amount,
- paymentType: data.paymentType,
- };
- createReservationWithPayment(reservationPaymentRequest)
- .then(() => {
- alert("예약이 완료되었습니다.");
- window.location.href = "/";
- })
- .catch(handleError);
- }).catch(function (error: any) {
- // This is a client-side error from Toss Payments, not our API
- console.error("Payment request error:", error);
- alert("결제 요청 중 오류가 발생했습니다.");
- });
- };
-
- const handleWaiting = () => {
- if (!selectedDate || !selectedTheme || !selectedTime) {
- alert('날짜, 테마, 시간을 모두 선택해주세요.');
- return;
- }
-
- const reservationData = {
- date: selectedDate.toLocaleDateString('en-CA'),
- themeId: selectedTheme,
- timeId: selectedTime.id,
- };
-
- createWaiting(reservationData)
- .then(() => {
- alert('예약 대기가 완료되었습니다.');
- window.location.href = "/";
- })
- .catch(handleError);
- }
-
- const isReserveButtonDisabled = !selectedDate || !selectedTheme || !selectedTime || !selectedTime.isAvailable;
- const isWaitButtonDisabled = !selectedDate || !selectedTheme || !selectedTime || selectedTime.isAvailable;
-
- return (
- <>
-
-
예약 페이지
-
-
-
날짜 선택
-
- setSelectedDate(date)}
- options={{ inline: true, defaultDate: new Date() }}
- />
-
-
-
-
-
테마 선택
-
- {themes.map(theme => (
-
setSelectedTheme(theme.id)}>
- {theme.name}
-
- ))}
-
-
-
-
-
시간 선택
-
- {times.length > 0 ? times.map(time => (
-
setSelectedTime({ id: time.id, isAvailable: time.isAvailable })}>
- {time.startAt}
-
- )) :
선택할 수 있는 시간이 없습니다.
}
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-export default ReservationPage;
diff --git a/frontend/src/pages/v2/ReservationStep1PageV21.tsx b/frontend/src/pages/ReservationStep1Page.tsx
similarity index 85%
rename from frontend/src/pages/v2/ReservationStep1PageV21.tsx
rename to frontend/src/pages/ReservationStep1Page.tsx
index 0910bfc8..b73f607a 100644
--- a/frontend/src/pages/v2/ReservationStep1PageV21.tsx
+++ b/frontend/src/pages/ReservationStep1Page.tsx
@@ -1,49 +1,19 @@
-import { isLoginRequiredError } from '@_api/apiClient';
-import { holdSchedule, findAvailableThemesByDate, findSchedules } from '@_api/schedule/scheduleAPI';
-import { ScheduleStatus, type ScheduleRetrieveResponse } from '@_api/schedule/scheduleTypes';
-import { findThemesByIds } from '@_api/theme/themeAPI';
-import { Difficulty } from '@_api/theme/themeTypes';
+import {isLoginRequiredError} from '@_api/apiClient';
+import {findAvailableThemesByDate, findSchedules, holdSchedule} from '@_api/schedule/scheduleAPI';
+import {type ScheduleRetrieveResponse, ScheduleStatus} from '@_api/schedule/scheduleTypes';
+import {findThemesByIds} from '@_api/theme/themeAPI';
+import {mapThemeResponse, 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 { formatDate, formatTime } from 'src/util/DateTimeFormatter';
+import React, {useEffect, useState} from 'react';
+import {useLocation, useNavigate} from 'react-router-dom';
+import {formatDate, formatTime} from 'src/util/DateTimeFormatter';
-interface ThemeV21 {
- id: string;
- name: string;
- difficulty: Difficulty;
- description: string;
- thumbnailUrl: string;
- price: number;
- minParticipants: number;
- maxParticipants: number;
- expectedMinutesFrom: number;
- expectedMinutesTo: number;
- availableMinutes: number;
-}
-const getDifficultyText = (difficulty: Difficulty): string => {
- switch (difficulty) {
- case Difficulty.VERY_EASY:
- return '매우 쉬움';
- case Difficulty.EASY:
- return '쉬움';
- case Difficulty.NORMAL:
- return '보통';
- case Difficulty.HARD:
- return '어려움';
- case Difficulty.VERY_HARD:
- return '매우 어려움';
- default:
- return difficulty;
- }
-};
-
-const ReservationStep1PageV21: React.FC = () => {
+const ReservationStep1Page: React.FC = () => {
const [selectedDate, setSelectedDate] = useState(new Date());
const [viewDate, setViewDate] = useState(new Date()); // For carousel
- const [themes, setThemes] = useState([]);
- const [selectedTheme, setSelectedTheme] = useState(null);
+ const [themes, setThemes] = useState([]);
+ const [selectedTheme, setSelectedTheme] = useState(null);
const [schedules, setSchedules] = useState([]);
const [selectedSchedule, setSelectedSchedule] = useState(null);
const [isThemeModalOpen, setIsThemeModalOpen] = useState(false);
@@ -77,9 +47,17 @@ const ReservationStep1PageV21: React.FC = () => {
}
})
.then(themeResponse => {
- setThemes(themeResponse.themes as ThemeV21[]);
+ setThemes(themeResponse.themes.map(mapThemeResponse));
+ })
+ .catch((err) => {
+ if (isLoginRequiredError(err)) {
+ setThemes([]);
+ } else {
+ const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
+ alert(message);
+ console.error(err);
+ }
})
- .catch(handleError)
.finally(() => {
setSelectedTheme(null);
setSchedules([]);
@@ -96,7 +74,16 @@ const ReservationStep1PageV21: React.FC = () => {
setSchedules(res.schedules);
setSelectedSchedule(null);
})
- .catch(handleError);
+ .catch((err) => {
+ if (isLoginRequiredError(err)) {
+ setSchedules([]);
+ } else {
+ const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
+ alert(message);
+ console.error(err);
+ }
+ setSelectedSchedule(null);
+ });
}
}, [selectedDate, selectedTheme]);
@@ -117,7 +104,7 @@ const ReservationStep1PageV21: React.FC = () => {
holdSchedule(selectedSchedule.id)
.then(() => {
- navigate('/v2/reservation/form', {
+ navigate('/reservation/form', {
state: {
scheduleId: selectedSchedule.id,
theme: selectedTheme,
@@ -197,7 +184,7 @@ const ReservationStep1PageV21: React.FC = () => {
);
};
- const openThemeModal = (theme: ThemeV21) => {
+ const openThemeModal = (theme: ThemeInfoResponse) => {
setSelectedTheme(theme);
setIsThemeModalOpen(true);
};
@@ -237,7 +224,7 @@ const ReservationStep1PageV21: React.FC = () => {
{theme.name}
1인당 요금: {theme.price.toLocaleString()}원
-
난이도: {getDifficultyText(theme.difficulty)}
+
난이도: {theme.difficulty}
참여 가능 인원: {theme.minParticipants} ~ {theme.maxParticipants}명
예상 소요 시간: {theme.expectedMinutesFrom} ~ {theme.expectedMinutesTo}분
이용 가능 시간: {theme.availableMinutes}분
@@ -279,7 +266,7 @@ const ReservationStep1PageV21: React.FC = () => {
{selectedTheme.name}
테마 정보
-
난이도: {getDifficultyText(selectedTheme.difficulty)}
+
난이도: {selectedTheme.difficulty}
참여 인원: {selectedTheme.minParticipants} ~ {selectedTheme.maxParticipants}명
소요 시간: {selectedTheme.expectedMinutesFrom} ~ {selectedTheme.expectedMinutesTo}분
1인당 요금: {selectedTheme.price.toLocaleString()}원
@@ -313,4 +300,4 @@ const ReservationStep1PageV21: React.FC = () => {
);
};
-export default ReservationStep1PageV21;
+export default ReservationStep1Page;
\ No newline at end of file
diff --git a/frontend/src/pages/v2/ReservationStep2PageV21.tsx b/frontend/src/pages/ReservationStep2Page.tsx
similarity index 86%
rename from frontend/src/pages/v2/ReservationStep2PageV21.tsx
rename to frontend/src/pages/ReservationStep2Page.tsx
index 0da89b2a..3f96ef4e 100644
--- a/frontend/src/pages/v2/ReservationStep2PageV21.tsx
+++ b/frontend/src/pages/ReservationStep2Page.tsx
@@ -1,11 +1,11 @@
-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 {isLoginRequiredError} from '@_api/apiClient';
+import {confirmPayment} from '@_api/payment/paymentAPI';
+import {type PaymentConfirmRequest, PaymentType} from '@_api/payment/PaymentTypes';
+import {confirmReservation} from '@_api/reservation/reservationAPI';
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';
+import React, {useEffect, useRef} from 'react';
+import {useLocation, useNavigate} from 'react-router-dom';
+import {formatDate, formatTime} from 'src/util/DateTimeFormatter';
declare global {
interface Window {
@@ -13,7 +13,7 @@ declare global {
}
}
-const ReservationStep2PageV21: React.FC = () => {
+const ReservationStep2Page: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const paymentWidgetRef = useRef
(null);
@@ -35,7 +35,7 @@ const ReservationStep2PageV21: React.FC = () => {
useEffect(() => {
if (!reservationId) {
alert('잘못된 접근입니다.');
- navigate('/v2-1/reservation');
+ navigate('/reservation');
return;
}
@@ -85,7 +85,7 @@ const ReservationStep2PageV21: React.FC = () => {
})
.then(() => {
alert('결제가 완료되었어요!');
- navigate('/v2-1/reservation/success', {
+ navigate('/reservation/success', {
state: {
themeName,
date,
@@ -128,4 +128,4 @@ const ReservationStep2PageV21: React.FC = () => {
);
};
-export default ReservationStep2PageV21;
+export default ReservationStep2Page;
diff --git a/frontend/src/pages/v2/ReservationSuccessPageV21.tsx b/frontend/src/pages/ReservationSuccessPage.tsx
similarity index 79%
rename from frontend/src/pages/v2/ReservationSuccessPageV21.tsx
rename to frontend/src/pages/ReservationSuccessPage.tsx
index defa9797..5f86aebf 100644
--- a/frontend/src/pages/v2/ReservationSuccessPageV21.tsx
+++ b/frontend/src/pages/ReservationSuccessPage.tsx
@@ -1,9 +1,9 @@
import '@_css/reservation-v2-1.css'; // Reuse the new CSS
import React from 'react';
-import { Link, useLocation } from 'react-router-dom';
-import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
+import {Link, useLocation} from 'react-router-dom';
+import {formatDate, formatTime} from 'src/util/DateTimeFormatter';
-const ReservationSuccessPageV21: React.FC = () => {
+const ReservationSuccessPage: React.FC = () => {
const location = useLocation();
const { themeName, date, startAt } = (location.state as {
themeName: string;
@@ -25,7 +25,7 @@ const ReservationSuccessPageV21: React.FC = () => {
시간: {formattedTime}
-
+
내 예약 목록
@@ -36,4 +36,4 @@ const ReservationSuccessPageV21: React.FC = () => {
);
};
-export default ReservationSuccessPageV21;
+export default ReservationSuccessPage;
diff --git a/frontend/src/pages/SignupPage.tsx b/frontend/src/pages/SignupPage.tsx
index ed2c208d..845ac67d 100644
--- a/frontend/src/pages/SignupPage.tsx
+++ b/frontend/src/pages/SignupPage.tsx
@@ -1,42 +1,146 @@
-import React, { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-import { signup } from '@_api/member/memberAPI';
-import type { SignupRequest } from '@_api/member/memberTypes';
+import {signup} from '@_api/user/userAPI';
+import type {UserCreateRequest, UserCreateResponse} from '@_api/user/userTypes';
+import '@_css/signup-page-v2.css';
+import React, {useEffect, useState} from 'react';
+import {useNavigate} from 'react-router-dom';
+
+const MIN_PASSWORD_LENGTH = 8;
const SignupPage: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
+ const [phone, setPhone] = useState('');
+ const [errors, setErrors] = useState
>({});
+ const [hasSubmitted, setHasSubmitted] = useState(false);
+
const navigate = useNavigate();
- const handleSignup = async () => {
- const request: SignupRequest = { email, password, name };
- await signup(request)
- .then((response) => {
- alert(`${response.name}님, 회원가입을 축하드려요. 로그인 후 이용해주세요!`);
- navigate('/login')
- })
- .catch(error => {
- console.error(error);
- });
+ const validate = () => {
+ const newErrors: Record = {};
+
+ if (!name.trim()) {
+ newErrors.name = '이름을 입력해주세요.';
+ }
+ if (!email.trim()) {
+ newErrors.email = '이메일을 입력해주세요.';
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+ newErrors.email = '올바른 이메일 형식을 입력해주세요.';
+ }
+ if (password.length < MIN_PASSWORD_LENGTH) {
+ newErrors.password = `비밀번호는 최소 ${MIN_PASSWORD_LENGTH}자리 이상이어야 합니다.`;
+ }
+ if (!phone.trim()) {
+ newErrors.phone = '전화번호를 입력해주세요.';
+ } else if (!/^010([0-9]{3,4})([0-9]{4})$/.test(phone)) {
+ newErrors.phone = '올바른 휴대폰 번호 형식이 아닙니다. (예: 01012345678)';
+ }
+
+ return newErrors;
+ };
+
+ // 제출 이후에는 입력값이 바뀔 때마다 다시 validate 실행
+ useEffect(() => {
+ if (hasSubmitted) {
+ setErrors(validate());
+ }
+ }, [email, password, name, phone, hasSubmitted]);
+
+ const handleSignup = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setHasSubmitted(true);
+
+ const newErrors = validate();
+ setErrors(newErrors);
+
+ if (Object.keys(newErrors).length > 0) return;
+
+ const request: UserCreateRequest = { email, password, name, phone, regionCode: null };
+ try {
+ const response: UserCreateResponse = await signup(request);
+ alert(`${response.name}님, 회원가입을 축하드려요. 로그인 후 이용해주세요!`);
+ navigate('/login');
+ } catch (error: any) {
+ const message =
+ error.response?.data?.message ||
+ '회원가입에 실패했어요. 입력 정보를 확인해주세요.';
+ alert(message);
+ console.error(error);
+ }
};
return (
-
-
Signup
-
-
- setEmail(e.target.value)} />
-
-
-
- setPassword(e.target.value)} />
-
-
-
- setName(e.target.value)} />
-
-
+
);
};
diff --git a/frontend/src/pages/admin/AdminLayout.tsx b/frontend/src/pages/admin/AdminLayout.tsx
index 47034a06..e5ca8e7d 100644
--- a/frontend/src/pages/admin/AdminLayout.tsx
+++ b/frontend/src/pages/admin/AdminLayout.tsx
@@ -1,4 +1,4 @@
-import React, { type ReactNode } from 'react';
+import React, {type ReactNode} from 'react';
import AdminNavbar from './AdminNavbar';
interface AdminLayoutProps {
diff --git a/frontend/src/pages/admin/AdminNavbar.tsx b/frontend/src/pages/admin/AdminNavbar.tsx
index 20444525..8928fb1f 100644
--- a/frontend/src/pages/admin/AdminNavbar.tsx
+++ b/frontend/src/pages/admin/AdminNavbar.tsx
@@ -1,7 +1,7 @@
import React from 'react';
-import { Link, useNavigate } from 'react-router-dom';
-import { useAuth } from '../../context/AuthContext';
-import '../../css/navbar.css';
+import {Link, useNavigate} from 'react-router-dom';
+import {useAuth} from '@_context/AuthContext';
+import '@_css/navbar.css';
const AdminNavbar: React.FC = () => {
const { loggedIn, userName, logout } = useAuth();
@@ -21,10 +21,7 @@ const AdminNavbar: React.FC = () => {