refactor: 회원 예약 조회에서의 변경된 스펙 프론트엔드 반영

This commit is contained in:
이상진 2025-09-19 18:31:10 +09:00
parent c64e613a2b
commit 1bd2292ea0
6 changed files with 110 additions and 115 deletions

View File

@ -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> => {

View File

@ -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;

View File

@ -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');
} }

View File

@ -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 --- */

View File

@ -1,17 +1,18 @@
import {cancelPayment} from '@_api/payment/paymentAPI'; 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 ReservationDetail,
type ReservationSummaryRetrieveResponse 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:
@ -22,81 +23,12 @@ const getReservationStatus = (reservation: ReservationSummaryRetrieveResponse):
} }
return { className: 'status-confirmed', text: '예약확정' }; return { className: 'status-confirmed', text: '예약확정' };
case ReservationStatus.PENDING: case ReservationStatus.PENDING:
return { className: 'status-pending', text: '입금대기' }; return { className: 'status-pending', text: '입금대기' };
default: default:
return { className: `status-${reservation.status.toLowerCase()}`, text: reservation.status }; 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 --- // --- 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>
@ -147,7 +79,7 @@ const ReservationDetailView: React.FC<{
const renderPaymentSubDetails = (payment: PaymentRetrieveResponse) => { const renderPaymentSubDetails = (payment: PaymentRetrieveResponse) => {
if (!payment.detail) { if (!payment.detail) {
return <p> . .</p>; return <p> . .</p>;
} }
const { detail } = payment; const { detail } = payment;
@ -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>
); );

View File

@ -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);
};