diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e85af96a..b95ec603 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -23,6 +23,7 @@ import ReservationSuccessPageV21 from './pages/v2/ReservationSuccessPageV21'; import HomePageV2 from './pages/v2/HomePageV2'; import LoginPageV2 from './pages/v2/LoginPageV2'; import SignupPageV2 from './pages/v2/SignupPageV2'; +import ReservationFormPage from './pages/v2/ReservationFormPage'; import AdminThemeEditPage from './pages/admin/AdminThemeEditPage'; import AdminSchedulePage from './pages/admin/AdminSchedulePage'; @@ -72,6 +73,7 @@ function App() { {/* V2.1 Reservation Flow */} } /> + } /> } /> } /> diff --git a/frontend/src/api/member/memberTypes.ts b/frontend/src/api/member/memberTypes.ts index 32d4ab17..6dc36555 100644 --- a/frontend/src/api/member/memberTypes.ts +++ b/frontend/src/api/member/memberTypes.ts @@ -17,3 +17,9 @@ export interface SignupResponse { id: string; name: string; } + +export interface MemberSummaryRetrieveResponse { + id: string; + name: string; + email: string; +} diff --git a/frontend/src/api/payment/PaymentTypes.ts b/frontend/src/api/payment/PaymentTypes.ts new file mode 100644 index 00000000..93b015df --- /dev/null +++ b/frontend/src/api/payment/PaymentTypes.ts @@ -0,0 +1,73 @@ +export interface PaymentConfirmRequest { + paymentKey: string; + orderId: string; + amount: number; + paymentType: PaymentType; +} + +export interface PaymentCancelRequest { + reservationId: string, + cancelReason: String +} + +// V2 types +export const PaymentType = { + NORMAL: 'NORMAL', + BILLING: 'BILLING', + BRANDPAY: 'BRANDPAY' +} as const; + +export type PaymentType = + | typeof PaymentType.NORMAL + | typeof PaymentType.BILLING + | typeof PaymentType.BRANDPAY; + +export interface PaymentCreateResponseV2 { + paymentId: string; + detailId: string; +} + +export interface PaymentRetrieveResponse { + orderId: string; + totalAmount: number; + method: string; + status: 'DONE' | 'CANCELED'; + requestedAt: string; + approvedAt: string; + detail: CardPaymentDetail | BankTransferPaymentDetail | EasyPayPrepaidPaymentDetail; + cancellation?: CanceledPaymentDetailResponse; +} + +export interface CardPaymentDetail { + type: 'CARD'; + issuerCode: string; + cardType: 'CREDIT' | 'CHECK' | 'GIFT'; + ownerType: 'PERSONAL' | 'CORPORATE'; + cardNumber: string; + amount: number; + approvalNumber: string; + installmentPlanMonths: number; + isInterestFree: boolean; + easypayProviderName?: string; + easypayDiscountAmount?: number; +} + +export interface BankTransferPaymentDetail { + type: 'BANK_TRANSFER'; + bankName: string; + settlementStatus: string; +} + +export interface EasyPayPrepaidPaymentDetail { + type: 'EASYPAY_PREPAID'; + providerName: string; + amount: number; + discountAmount: number; +} + +export interface CanceledPaymentDetailResponse { + cancellationRequestedAt: string; // ISO 8601 format + cancellationApprovedAt: string; // ISO 8601 format + cancelReason: string; + canceledBy: string; +} diff --git a/frontend/src/api/payment/paymentAPI.ts b/frontend/src/api/payment/paymentAPI.ts new file mode 100644 index 00000000..c5481ec8 --- /dev/null +++ b/frontend/src/api/payment/paymentAPI.ts @@ -0,0 +1,10 @@ +import apiClient from "@_api/apiClient"; +import type { PaymentCancelRequest, PaymentConfirmRequest, PaymentCreateResponseV2 } from "./PaymentTypes"; + +export const confirmPayment = async (reservationId: string, request: PaymentConfirmRequest): Promise => { + return await apiClient.post(`/payments?reservationId=${reservationId}`, request); +}; + +export const cancelPayment = async (request: PaymentCancelRequest): Promise => { + return await apiClient.post(`/payments/cancel`, request); +}; diff --git a/frontend/src/api/reservation/reservationAPI.ts b/frontend/src/api/reservation/reservationAPI.ts index 01a39c4a..ca370e74 100644 --- a/frontend/src/api/reservation/reservationAPI.ts +++ b/frontend/src/api/reservation/reservationAPI.ts @@ -85,10 +85,7 @@ export const confirmReservationPayment = async (id: string, data: ReservationPay return await apiClient.post(`/v2/reservations/${id}/pay`, data, true); }; -// POST /v2/reservations/{id}/cancel -export const cancelReservationV2 = async (id: string, cancelReason: string): Promise => { - return await apiClient.post(`/v2/reservations/${id}/cancel`, { cancelReason }, true); -}; + // GET /v2/reservations export const fetchMyReservationsV2 = async (): Promise => { diff --git a/frontend/src/api/reservation/reservationAPIV2.ts b/frontend/src/api/reservation/reservationAPIV2.ts index e69de29b..01992108 100644 --- a/frontend/src/api/reservation/reservationAPIV2.ts +++ b/frontend/src/api/reservation/reservationAPIV2.ts @@ -0,0 +1,23 @@ +import apiClient from '../apiClient'; +import type { PendingReservationCreateRequest, PendingReservationCreateResponse, ReservationDetailRetrieveResponse, ReservationSummaryRetrieveListResponse } from './reservationTypesV2'; + +export const createPendingReservation = async (request: PendingReservationCreateRequest): Promise => { + return await apiClient.post('/reservations/pending', request); +}; + +export const confirmReservation = async (reservationId: string): Promise => { + await apiClient.post(`/reservations/${reservationId}/confirm`, {}); +}; + + +export const cancelReservation = async (id: string, cancelReason: string): Promise => { + return await apiClient.post(`/reservations/${id}/cancel`, { cancelReason }, true); +}; + +export const fetchSummaryByMember = async (): Promise => { + return await apiClient.get('/reservations/summary'); +} + +export const fetchDetailById = async (reservationId: string): Promise => { + return await apiClient.get(`/reservations/${reservationId}/detail`); +} diff --git a/frontend/src/api/reservation/reservationTypes.ts b/frontend/src/api/reservation/reservationTypes.ts index 5501ed8c..29d20a57 100644 --- a/frontend/src/api/reservation/reservationTypes.ts +++ b/frontend/src/api/reservation/reservationTypes.ts @@ -1,4 +1,5 @@ -import type { MemberRetrieveResponse } from '@_api/member/memberTypes'; +import type { MemberRetrieveResponse, MemberSummaryRetrieveResponse } from '@_api/member/memberTypes'; +import type { PaymentRetrieveResponse, PaymentType } from '@_api/payment/PaymentTypes'; import type { ThemeRetrieveResponse } from '@_api/theme/themeTypes'; import type { TimeRetrieveResponse } from '@_api/time/timeTypes'; @@ -77,18 +78,6 @@ export interface ReservationSearchQuery { dateTo?: string; } -// V2 types -export const PaymentType = { - NORMAL: 'NORMAL', - BILLING: 'BILLING', - BRANDPAY: 'BRANDPAY' -} as const; - -export type PaymentType = - | typeof PaymentType.NORMAL - | typeof PaymentType.BILLING - | typeof PaymentType.BRANDPAY; - export const PaymentStatus = { IN_PROGRESS: '결제 진행 중', DONE: '결제 완료', @@ -123,7 +112,7 @@ export interface ReservationPaymentRequest { paymentKey: string; orderId: string; amount: number; - paymentType: PaymentType + paymentType: PaymentType; } export interface ReservationPaymentResponse { @@ -133,75 +122,14 @@ export interface ReservationPaymentResponse { paymentStatus: PaymentStatus; } -export interface ReservationSummaryV2 { - id: string; - themeName: string; - date: string; - startAt: string; - status: string; // 'CONFIRMED', 'CANCELED_BY_USER', etc. -} -export interface ReservationSummaryListV2 { - reservations: ReservationSummaryV2[]; -} export interface ReservationDetailV2 { id: string; - user: UserDetailV2; + user: MemberSummaryRetrieveResponse; 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 { - orderId: string; - totalAmount: number; - method: string; - status: 'DONE' | 'CANCELED'; - requestedAt: string; - approvedAt: string; - detail: CardPaymentDetailV2 | BankTransferPaymentDetailV2 | EasyPayPrepaidPaymentDetailV2; -} - -export interface CardPaymentDetailV2 { - type: 'CARD'; - issuerCode: string; - cardType: 'CREDIT' | 'CHECK' | 'GIFT'; - ownerType: 'PERSONAL' | 'CORPORATE'; - cardNumber: string; - amount: number; - approvalNumber: string; - installmentPlanMonths: number; - isInterestFree: boolean; - easypayProviderName?: string; - easypayDiscountAmount?: number; -} - -export interface BankTransferPaymentDetailV2 { - type: 'BANK_TRANSFER'; - bankName: string; - settlementStatus: string; -} - -export interface EasyPayPrepaidPaymentDetailV2 { - type: 'EASYPAY_PREPAID'; - providerName: string; - amount: number; - discountAmount: number; -} - -export interface CancellationV2 { - cancellationRequestedAt: string; // ISO 8601 format - cancellationApprovedAt: string; // ISO 8601 format - cancelReason: string; - canceledBy: string; + payment: PaymentRetrieveResponse; } diff --git a/frontend/src/api/reservation/reservationTypesV2.ts b/frontend/src/api/reservation/reservationTypesV2.ts new file mode 100644 index 00000000..3dc595fa --- /dev/null +++ b/frontend/src/api/reservation/reservationTypesV2.ts @@ -0,0 +1,58 @@ +import type { MemberSummaryRetrieveResponse } from "@_api/member/memberTypes"; +import type { PaymentRetrieveResponse } from "@_api/payment/PaymentTypes"; + +export const ReservationStatusV2 = { + PENDING: 'PENDING', + CONFIRMED: 'CONFIRMED', + CANCELED: 'CANCELED', + FAILED: 'FAILED', + EXPIRED: 'EXPIRED' +} as const; + +export type ReservationStatusV2 = + | typeof ReservationStatusV2.PENDING + | typeof ReservationStatusV2.CONFIRMED + | typeof ReservationStatusV2.CANCELED + | typeof ReservationStatusV2.FAILED + | typeof ReservationStatusV2.EXPIRED; + +export interface PendingReservationCreateRequest { + scheduleId: string, + reserverName: string, + reserverContact: string, + participantCount: number, + requirement: string +} + +export interface PendingReservationCreateResponse { + id: string +} + +export interface ReservationSummaryRetrieveResponse { + id: string; + themeName: string; + date: string; + startAt: string; + status: ReservationStatusV2; +} + +export interface ReservationSummaryRetrieveListResponse { + reservations: ReservationSummaryRetrieveResponse[]; +} + +export interface ReservationDetailRetrieveResponse { + id: string; + member: MemberSummaryRetrieveResponse; + applicationDateTime: string; + payment: PaymentRetrieveResponse; +} + +export interface ReservationDetail { + id: string; + themeName: string; + date: string; + startAt: string; + member: MemberSummaryRetrieveResponse; + applicationDateTime: string; + payment: PaymentRetrieveResponse; +} diff --git a/frontend/src/api/schedule/scheduleAPI.ts b/frontend/src/api/schedule/scheduleAPI.ts index 403a5204..f828fa2b 100644 --- a/frontend/src/api/schedule/scheduleAPI.ts +++ b/frontend/src/api/schedule/scheduleAPI.ts @@ -30,3 +30,7 @@ export const updateSchedule = async (id: string, request: ScheduleUpdateRequest) export const deleteSchedule = async (id: string): Promise => { await apiClient.del(`/schedules/${id}`); }; + +export const holdSchedule = async (id: string): Promise => { + await apiClient.patch(`/schedules/${id}/hold`, {}); +}; diff --git a/frontend/src/api/schedule/scheduleTypes.ts b/frontend/src/api/schedule/scheduleTypes.ts index 73550b46..6230cf01 100644 --- a/frontend/src/api/schedule/scheduleTypes.ts +++ b/frontend/src/api/schedule/scheduleTypes.ts @@ -1,6 +1,6 @@ export enum ScheduleStatus { AVAILABLE = 'AVAILABLE', - PENDING = 'PENDING', + HOLD = 'HOLD', RESERVED = 'RESERVED', BLOCKED = 'BLOCKED', } diff --git a/frontend/src/css/reservation-v2-1.css b/frontend/src/css/reservation-v2-1.css index 6e56c6ba..76820556 100644 --- a/frontend/src/css/reservation-v2-1.css +++ b/frontend/src/css/reservation-v2-1.css @@ -417,4 +417,81 @@ } .modal-actions .confirm-button:hover { background-color: #1B64DA; -} \ No newline at end of file +} + +/* Styles for ReservationFormPage */ +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + font-weight: bold; + margin-bottom: 8px; + color: #333; +} + +.form-group input[type="text"], +.form-group input[type="tel"], +.form-group input[type="number"], +.form-group textarea { + width: 100%; + padding: 12px; + border: 1px solid #ccc; + border-radius: 8px; + font-size: 16px; + box-sizing: border-box; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.form-group input:focus, .form-group textarea:focus { + outline: none; + border-color: #3182F6; + box-shadow: 0 0 0 3px rgba(49, 130, 246, 0.2); +} + +.form-group textarea { + resize: vertical; + min-height: 100px; +} + +.participant-control { + display: flex; + align-items: center; +} + +.participant-control input { + text-align: center; + border-left: none; + border-right: none; + width: 60px; + border-radius: 0; +} + +.participant-control button { + width: 44px; + height: 44px; + border: 1px solid #ccc; + background-color: #f0f0f0; + font-size: 20px; + cursor: pointer; + transition: background-color 0.2s; +} + +.participant-control button:hover:not(:disabled) { + background-color: #e0e0e0; +} + +.participant-control button:disabled { + background-color: #e9ecef; + cursor: not-allowed; + color: #aaa; +} + +.participant-control button:first-of-type { + border-radius: 8px 0 0 8px; +} + +.participant-control button:last-of-type { + border-radius: 0 8px 8px 0; +} diff --git a/frontend/src/pages/admin/AdminSchedulePage.tsx b/frontend/src/pages/admin/AdminSchedulePage.tsx index 28657fe6..7f8c5793 100644 --- a/frontend/src/pages/admin/AdminSchedulePage.tsx +++ b/frontend/src/pages/admin/AdminSchedulePage.tsx @@ -11,7 +11,7 @@ const getScheduleStatusText = (status: ScheduleStatus): string => { switch (status) { case ScheduleStatus.AVAILABLE: return '예약 가능'; - case ScheduleStatus.PENDING: + case ScheduleStatus.HOLD: return '예약 진행 중'; case ScheduleStatus.RESERVED: return '예약 완료'; diff --git a/frontend/src/pages/admin/AdminThemeEditPage.tsx b/frontend/src/pages/admin/AdminThemeEditPage.tsx index 88a8fc51..0121d5dd 100644 --- a/frontend/src/pages/admin/AdminThemeEditPage.tsx +++ b/frontend/src/pages/admin/AdminThemeEditPage.tsx @@ -193,7 +193,7 @@ const AdminThemeEditPage: React.FC = () => {
- +
diff --git a/frontend/src/pages/admin/ThemePage.tsx b/frontend/src/pages/admin/ThemePage.tsx index f2daaba4..68eec8b0 100644 --- a/frontend/src/pages/admin/ThemePage.tsx +++ b/frontend/src/pages/admin/ThemePage.tsx @@ -37,7 +37,7 @@ const AdminThemePage: React.FC = () => { navigate('/admin/theme/edit/new'); }; - const handleManageClick = (themeId: number) => { + const handleManageClick = (themeId: string) => { navigate(`/admin/theme/edit/${themeId}`); }; @@ -54,7 +54,7 @@ const AdminThemePage: React.FC = () => { 이름 난이도 - 가격 + 1인당 요금 공개여부 diff --git a/frontend/src/pages/v2/MyReservationPageV2.tsx b/frontend/src/pages/v2/MyReservationPageV2.tsx index ccbcb99e..a229b953 100644 --- a/frontend/src/pages/v2/MyReservationPageV2.tsx +++ b/frontend/src/pages/v2/MyReservationPageV2.tsx @@ -1,10 +1,8 @@ +import { cancelPayment } from '@_api/payment/paymentAPI'; +import type { PaymentRetrieveResponse } from '@_api/payment/PaymentTypes'; +import { cancelReservation, fetchDetailById, fetchSummaryByMember } from '@_api/reservation/reservationAPIV2'; +import type { ReservationDetail, ReservationSummaryRetrieveResponse } from '@_api/reservation/reservationTypesV2'; import React, { useEffect, useState } from 'react'; -import { - cancelReservationV2, - fetchMyReservationsV2, - 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 => { @@ -78,7 +76,7 @@ const formatCardDateTime = (dateStr: string, timeStr: string): string => { // --- Cancellation View Component --- const CancellationView: React.FC<{ - reservation: ReservationDetailV2; + reservation: ReservationDetail; onCancelSubmit: (reason: string) => void; onBack: () => void; isCancelling: boolean; @@ -119,13 +117,12 @@ const CancellationView: React.FC<{ }; -// --- Reservation Detail View Component --- const ReservationDetailView: React.FC<{ - reservation: ReservationDetailV2; + reservation: ReservationDetail; onGoToCancel: () => void; }> = ({ reservation, onGoToCancel }) => { - const renderPaymentDetails = (payment: PaymentV2) => { + const renderPaymentDetails = (payment: PaymentRetrieveResponse) => { const { detail } = payment; switch (detail.type) { @@ -178,8 +175,8 @@ const ReservationDetailView: React.FC<{

예약 정보

예약 테마: {reservation.themeName}

이용 예정일: {formatCardDateTime(reservation.date, reservation.startAt)}

-

예약자 이름: {reservation.user.name}

-

예약자 이메일: {reservation.user.email}

+

예약자 이름: {reservation.member.name}

+

예약자 이메일: {reservation.member.email}

예약 신청 일시: {formatDisplayDateTime(reservation.applicationDateTime)}

@@ -188,13 +185,13 @@ const ReservationDetailView: React.FC<{ {renderPaymentDetails(reservation.payment)}

결제 승인 일시: {formatDisplayDateTime(reservation.payment.approvedAt)}

- {reservation.cancellation && ( + {reservation.payment.cancellation && (

취소 정보

-

취소 요청 일시: {formatDisplayDateTime(reservation.cancellation.cancellationRequestedAt)}

-

환불 완료 일시: {formatDisplayDateTime(reservation.cancellation.cancellationApprovedAt)}

-

취소 사유: {reservation.cancellation.cancelReason}

-

취소 요청자: {reservation.cancellation.canceledBy == reservation.user.id ? '회원 본인' : '관리자'}

+

취소 요청 일시: {formatDisplayDateTime(reservation.payment.cancellation.cancellationRequestedAt)}

+

환불 완료 일시: {formatDisplayDateTime(reservation.payment.cancellation.cancellationApprovedAt)}

+

취소 사유: {reservation.payment.cancellation.cancelReason}

+

취소 요청자: {reservation.payment.cancellation.canceledBy == reservation.member.id ? '회원 본인' : '관리자'}

)} {reservation.payment.status !== 'CANCELED' && ( @@ -208,11 +205,11 @@ const ReservationDetailView: React.FC<{ // --- Main Page Component --- const MyReservationPageV2: React.FC = () => { - const [reservations, setReservations] = useState([]); + const [reservations, setReservations] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const [selectedReservation, setSelectedReservation] = useState(null); + const [selectedReservation, setSelectedReservation] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [isDetailLoading, setIsDetailLoading] = useState(false); const [detailError, setDetailError] = useState(null); @@ -223,7 +220,7 @@ const MyReservationPageV2: React.FC = () => { const loadReservations = async () => { try { setIsLoading(true); - const data = await fetchMyReservationsV2(); + const data = await fetchSummaryByMember(); setReservations(data.reservations); setError(null); } catch (err) { @@ -237,14 +234,21 @@ const MyReservationPageV2: React.FC = () => { loadReservations(); }, []); - const handleShowDetail = async (id: string) => { + const handleShowDetail = async (id: string, themeName: string, date: string, time: string) => { try { setIsDetailLoading(true); setDetailError(null); setModalView('detail'); - const detailData = await fetchReservationDetailV2(id); - console.log('상세 정보:', detailData); - setSelectedReservation(detailData); + const detailData = await fetchDetailById(id); + setSelectedReservation({ + id: detailData.id, + themeName: themeName, + date: date, + startAt: time, + member: detailData.member, + applicationDateTime: detailData.applicationDateTime, + payment: detailData.payment + }); setIsModalOpen(true); } catch (err) { setDetailError('예약 상세 정보를 불러오는 데 실패했습니다.'); @@ -268,16 +272,18 @@ const MyReservationPageV2: React.FC = () => { try { setIsCancelling(true); setDetailError(null); - await cancelReservationV2(selectedReservation.id, reason); + await cancelReservation(selectedReservation.id, reason); + cancelPayment({ reservationId: selectedReservation.id, cancelReason: reason }); alert('예약을 취소했어요. 결제 취소까지는 3-5일 정도 소요될 수 있어요.'); handleCloseModal(); - loadReservations(); // Refresh the list + await loadReservations(); // Refresh the list } catch (err) { setDetailError(err instanceof Error ? err.message : '예약 취소에 실패했습니다.'); } finally { - setIsCancelling(false); + setIsCancelling(true); } }; + console.log("reservations=", reservations); return (
@@ -289,14 +295,13 @@ const MyReservationPageV2: React.FC = () => { {!isLoading && !error && (
{reservations.map((res) => ( - console.log(res), -
+

{res.themeName}

{formatCardDateTime(res.date, res.startAt)}

+
+ ); + } + + return ( +
+

예약 정보 입력

+ +
+

예약 내용 확인

+

테마: {theme.name}

+

날짜: {formatDate(date)}

+

시간: {formatTime(time)}

+
+ +
+

예약자 정보

+
+ + setReserverName(e.target.value)} /> +
+
+ + setReserverContact(e.target.value)} placeholder="'-' 없이 입력"/> +
+
+ +
+ setParticipantCount(Math.max(theme.minParticipants, Math.min(theme.maxParticipants, Number(e.target.value))))} + min={theme.minParticipants} + max={theme.maxParticipants} + /> +
+
+
+ +