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