[#44] 매장 기능 도입 #45

Merged
pricelees merged 116 commits from feat/#44 into main 2025-09-20 03:15:06 +00:00
6 changed files with 110 additions and 115 deletions
Showing only changes of commit 1bd2292ea0 - Show all commits

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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