generated from pricelees/issue-pr-template
refactor: 회원 예약 조회에서의 변경된 스펙 프론트엔드 반영
This commit is contained in:
parent
c64e613a2b
commit
1bd2292ea0
@ -4,7 +4,7 @@ import type {
|
||||
PendingReservationCreateRequest,
|
||||
PendingReservationCreateResponse,
|
||||
ReservationDetailRetrieveResponse,
|
||||
ReservationSummaryRetrieveListResponse
|
||||
ReservationOverviewListResponse
|
||||
} from './reservationTypes';
|
||||
|
||||
export const createPendingReservation = async (request: PendingReservationCreateRequest): Promise<PendingReservationCreateResponse> => {
|
||||
@ -20,8 +20,8 @@ export const cancelReservation = async (id: string, cancelReason: string): Promi
|
||||
return await apiClient.post(`/reservations/${id}/cancel`, { cancelReason }, true);
|
||||
};
|
||||
|
||||
export const fetchSummaryByMember = async (): Promise<ReservationSummaryRetrieveListResponse> => {
|
||||
return await apiClient.get<ReservationSummaryRetrieveListResponse>('/reservations/summary');
|
||||
export const fetchSummaryByMember = async (): Promise<ReservationOverviewListResponse> => {
|
||||
return await apiClient.get<ReservationOverviewListResponse>('/reservations/summary');
|
||||
}
|
||||
|
||||
export const fetchDetailById = async (reservationId: string): Promise<ReservationDetailRetrieveResponse> => {
|
||||
|
||||
@ -46,30 +46,38 @@ export interface PendingReservationCreateResponse {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface ReservationSummaryRetrieveResponse {
|
||||
export interface ReservationOverviewResponse {
|
||||
id: string;
|
||||
storeName: string;
|
||||
themeName: string;
|
||||
date: string;
|
||||
startAt: string;
|
||||
startFrom: string;
|
||||
endAt: string;
|
||||
status: ReservationStatus;
|
||||
}
|
||||
|
||||
export interface ReservationSummaryRetrieveListResponse {
|
||||
reservations: ReservationSummaryRetrieveResponse[];
|
||||
export interface ReservationOverviewListResponse {
|
||||
reservations: ReservationOverviewResponse[];
|
||||
}
|
||||
|
||||
export interface ReserverInfo {
|
||||
name: string;
|
||||
contact: string;
|
||||
participantCount: number;
|
||||
requirement: string;
|
||||
}
|
||||
|
||||
export interface ReservationDetailRetrieveResponse {
|
||||
id: string;
|
||||
reserver: ReserverInfo;
|
||||
user: UserContactRetrieveResponse;
|
||||
applicationDateTime: string;
|
||||
payment: PaymentRetrieveResponse;
|
||||
}
|
||||
|
||||
export interface ReservationDetail {
|
||||
id: string;
|
||||
themeName: string;
|
||||
date: string;
|
||||
startAt: string;
|
||||
overview: ReservationOverviewResponse;
|
||||
reserver: ReserverInfo;
|
||||
user: UserContactRetrieveResponse;
|
||||
applicationDateTime: string;
|
||||
payment?: PaymentRetrieveResponse;
|
||||
|
||||
@ -2,9 +2,9 @@ import apiClient from "@_api/apiClient";
|
||||
import type {UserContactRetrieveResponse, UserCreateRequest, UserCreateResponse} from "./userTypes";
|
||||
|
||||
export const signup = async (data: UserCreateRequest): Promise<UserCreateResponse> => {
|
||||
return await apiClient.post('/users', data, false);
|
||||
return await apiClient.post('/users', data);
|
||||
};
|
||||
|
||||
export const fetchContact = async (): Promise<UserContactRetrieveResponse> => {
|
||||
return await apiClient.get<UserContactRetrieveResponse>('/users/contact', true);
|
||||
return await apiClient.get<UserContactRetrieveResponse>('/users/contact');
|
||||
}
|
||||
|
||||
@ -49,10 +49,24 @@
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.summary-subdetails-v2 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0px;
|
||||
gap: 0px;
|
||||
}
|
||||
|
||||
.summary-store-name-v2 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #505a67;
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
.summary-details-v2 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.summary-theme-name-v2 {
|
||||
@ -65,7 +79,7 @@
|
||||
.summary-datetime-v2 {
|
||||
font-size: 16px;
|
||||
color: #505a67;
|
||||
margin: 0;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* --- Status Badge --- */
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
import {cancelPayment} from '@_api/payment/paymentAPI';
|
||||
import type {PaymentRetrieveResponse} from '@_api/payment/PaymentTypes';
|
||||
import {cancelReservation, fetchDetailById, fetchSummaryByMember} from '@_api/reservation/reservationAPI';
|
||||
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
|
||||
type ReservationDetail,
|
||||
type ReservationOverviewResponse
|
||||
} from '@_api/reservation/reservationTypes';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import '@_css/my-reservation-v2.css';
|
||||
import { formatDate, formatDisplayDateTime, formatTime } from '@_util/DateTimeFormatter';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
const getReservationStatus = (reservation: ReservationSummaryRetrieveResponse): { className: string, text: string } => {
|
||||
const getReservationStatus = (reservation: ReservationOverviewResponse): { className: string, text: string } => {
|
||||
const now = new Date();
|
||||
const reservationDateTime = new Date(`${reservation.date}T${reservation.startAt}`);
|
||||
const reservationDateTime = new Date(`${reservation.date}T${reservation.startFrom}`);
|
||||
|
||||
switch (reservation.status) {
|
||||
case ReservationStatus.CANCELED:
|
||||
@ -28,75 +29,6 @@ const getReservationStatus = (reservation: ReservationSummaryRetrieveResponse):
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
@ -118,7 +50,7 @@ const CancellationView: React.FC<{
|
||||
<div className="cancellation-view-v2">
|
||||
<h3>취소 정보 확인</h3>
|
||||
<div className="cancellation-summary-v2 modal-info-grid">
|
||||
<p><strong>테마:</strong><span>{reservation.themeName}</span></p>
|
||||
<p><strong>테마:</strong><span>{reservation.overview.themeName}</span></p>
|
||||
<p><strong>신청 일시:</strong><span>{formatDisplayDateTime(reservation.applicationDateTime)}</span></p>
|
||||
{reservation.payment && <p><strong>결제 금액:</strong><span>{reservation.payment.totalAmount.toLocaleString()}원</span></p>}
|
||||
</div>
|
||||
@ -195,10 +127,11 @@ const ReservationDetailView: React.FC<{
|
||||
<>
|
||||
<div className="modal-section-v2 modal-info-grid">
|
||||
<h3>예약 정보</h3>
|
||||
<p><strong>예약 테마:</strong><span>{reservation.themeName}</span></p>
|
||||
<p><strong>이용 예정일:</strong><span>{formatCardDateTime(reservation.date, reservation.startAt)}</span></p>
|
||||
<p><strong>예약자 이름:</strong><span>{reservation.user.name}</span></p>
|
||||
<p><strong>예약자 이메일:</strong><span>{reservation.user.phone}</span></p>
|
||||
<p><strong>매장:</strong><span>{reservation.overview.storeName}</span></p>
|
||||
<p><strong>테마:</strong><span>{reservation.overview.themeName}</span></p>
|
||||
<p><strong>이용일시:</strong><span>{formatDate(reservation.overview.date)} {formatTime(reservation.overview.startFrom)} ~ {formatTime(reservation.overview.endAt)}</span></p>
|
||||
<p><strong>예약자 성함:</strong><span>{reservation.reserver.name}</span></p>
|
||||
<p><strong>예약자 연락처:</strong><span>{reservation.reserver.contact}</span></p>
|
||||
<p><strong>예약 신청 일시:</strong><span>{formatDisplayDateTime(reservation.applicationDateTime)}</span></p>
|
||||
</div>
|
||||
|
||||
@ -243,7 +176,7 @@ const ReservationDetailView: React.FC<{
|
||||
|
||||
// --- Main Page Component ---
|
||||
const MyReservationPage: React.FC = () => {
|
||||
const [reservations, setReservations] = useState<ReservationSummaryRetrieveResponse[]>([]);
|
||||
const [reservations, setReservations] = useState<ReservationOverviewResponse[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@ -272,17 +205,15 @@ const MyReservationPage: React.FC = () => {
|
||||
loadReservations();
|
||||
}, []);
|
||||
|
||||
const handleShowDetail = async (id: string, themeName: string, date: string, time: string) => {
|
||||
const handleShowDetail = async (overview: ReservationOverviewResponse) => {
|
||||
try {
|
||||
setIsDetailLoading(true);
|
||||
setDetailError(null);
|
||||
setModalView('detail');
|
||||
const detailData = await fetchDetailById(id);
|
||||
const detailData = await fetchDetailById(overview.id);
|
||||
setSelectedReservation({
|
||||
id: detailData.id,
|
||||
themeName: themeName,
|
||||
date: date,
|
||||
startAt: time,
|
||||
overview: overview,
|
||||
reserver: detailData.reserver,
|
||||
user: detailData.user,
|
||||
applicationDateTime: detailData.applicationDateTime,
|
||||
payment: detailData.payment
|
||||
@ -310,8 +241,8 @@ const MyReservationPage: React.FC = () => {
|
||||
try {
|
||||
setIsCancelling(true);
|
||||
setDetailError(null);
|
||||
await cancelPayment({ reservationId: selectedReservation.id, cancelReason: reason });
|
||||
await cancelReservation(selectedReservation.id, reason);
|
||||
await cancelPayment({ reservationId: selectedReservation.overview.id, cancelReason: reason });
|
||||
await cancelReservation(selectedReservation.overview.id, reason);
|
||||
alert('예약을 취소했어요. 결제 취소까지는 3-5일 정도 소요될 수 있어요.');
|
||||
handleCloseModal();
|
||||
await loadReservations(); // Refresh the list
|
||||
@ -338,15 +269,18 @@ const MyReservationPage: React.FC = () => {
|
||||
<div key={res.id} className={`reservation-summary-card-v2 ${status.className}`}>
|
||||
<div className="card-status-badge">{status.text}</div>
|
||||
<div className="summary-details-v2">
|
||||
<h3 className="summary-theme-name-v2">{res.themeName}</h3>
|
||||
<p className="summary-datetime-v2">{formatCardDateTime(res.date, res.startAt)}</p>
|
||||
<div><h3 className="summary-theme-name-v2">{res.themeName}</h3></div>
|
||||
<div className="summary-subdetails-v2">
|
||||
<p className="summary-store-name-v2">{res.storeName}</p>
|
||||
<p className="summary-datetime-v2">{formatDate(res.date)} {formatTime(res.startFrom)} ~ {formatTime(res.endAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleShowDetail(res.id, res.themeName, res.date, res.startAt)}
|
||||
onClick={() => handleShowDetail(res)}
|
||||
disabled={isDetailLoading}
|
||||
className="detail-button-v2"
|
||||
>
|
||||
{isDetailLoading && selectedReservation?.id === res.id ? '로딩중...' : '상세보기'}
|
||||
{isDetailLoading && selectedReservation?.overview.id === res.id ? '로딩중...' : '상세보기'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -33,3 +33,42 @@ export const formatTime = (timeStr: string) => {
|
||||
|
||||
return timePart;
|
||||
}
|
||||
|
||||
export 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);
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user