generated from pricelees/issue-pr-template
[#34] 회원 / 인증 도메인 재정의 #43
@ -1,19 +1,19 @@
|
|||||||
import apiClient from '@_api/apiClient';
|
import apiClient from '@_api/apiClient';
|
||||||
import type { LoginRequest, LoginResponse, LoginCheckResponse } from './authTypes';
|
import type { CurrentUserContext, LoginRequest, LoginSuccessResponse } from './authTypes';
|
||||||
|
|
||||||
|
|
||||||
export const login = async (data: LoginRequest): Promise<LoginResponse> => {
|
export const login = async (data: LoginRequest): Promise<LoginSuccessResponse> => {
|
||||||
const response = await apiClient.post<LoginResponse>('/login', data, false);
|
const response = await apiClient.post<LoginSuccessResponse>('/auth/login', data, false);
|
||||||
localStorage.setItem('accessToken', response.accessToken);
|
localStorage.setItem('accessToken', response.accessToken);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkLogin = async (): Promise<LoginCheckResponse> => {
|
export const checkLogin = async (): Promise<CurrentUserContext> => {
|
||||||
return await apiClient.get<LoginCheckResponse>('/login/check', true);
|
return await apiClient.get<CurrentUserContext>('/auth/login/check', true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const logout = async (): Promise<void> => {
|
export const logout = async (): Promise<void> => {
|
||||||
await apiClient.post('/logout', {}, true);
|
await apiClient.post('/auth/logout', {}, true);
|
||||||
localStorage.removeItem('accessToken');
|
localStorage.removeItem('accessToken');
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,14 +1,22 @@
|
|||||||
|
export const PrincipalType = {
|
||||||
|
ADMIN: 'ADMIN',
|
||||||
|
USER: 'USER',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type PrincipalType = typeof PrincipalType[keyof typeof PrincipalType];
|
||||||
|
|
||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
email: string;
|
account: string,
|
||||||
password: string;
|
password: string;
|
||||||
|
principalType: PrincipalType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginSuccessResponse {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginCheckResponse {
|
export interface CurrentUserContext {
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
role: 'ADMIN' | 'MEMBER';
|
type: PrincipalType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
import apiClient from "@_api/apiClient";
|
|
||||||
import type { MemberRetrieveListResponse, SignupRequest, SignupResponse } from "./memberTypes";
|
|
||||||
|
|
||||||
export const fetchMembers = async (): Promise<MemberRetrieveListResponse> => {
|
|
||||||
return await apiClient.get<MemberRetrieveListResponse>('/members', true);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const signup = async (data: SignupRequest): Promise<SignupResponse> => {
|
|
||||||
return await apiClient.post('/members', data, false);
|
|
||||||
};
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
export interface MemberRetrieveResponse {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MemberRetrieveListResponse {
|
|
||||||
members: MemberRetrieveResponse[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SignupRequest {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SignupResponse {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MemberSummaryRetrieveResponse {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
@ -34,8 +34,8 @@ export interface PaymentRetrieveResponse {
|
|||||||
status: 'DONE' | 'CANCELED';
|
status: 'DONE' | 'CANCELED';
|
||||||
requestedAt: string;
|
requestedAt: string;
|
||||||
approvedAt: string;
|
approvedAt: string;
|
||||||
detail: CardPaymentDetail | BankTransferPaymentDetail | EasyPayPrepaidPaymentDetail;
|
detail?: CardPaymentDetail | BankTransferPaymentDetail | EasyPayPrepaidPaymentDetail;
|
||||||
cancellation?: CanceledPaymentDetailResponse;
|
cancel?: CanceledPaymentDetailResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardPaymentDetail {
|
export interface CardPaymentDetail {
|
||||||
|
|||||||
@ -1,98 +0,0 @@
|
|||||||
import apiClient from "@_api/apiClient";
|
|
||||||
import type {
|
|
||||||
AdminReservationCreateRequest,
|
|
||||||
MyReservationRetrieveListResponse,
|
|
||||||
ReservationCreateRequest,
|
|
||||||
ReservationCreateResponse,
|
|
||||||
ReservationCreateWithPaymentRequest,
|
|
||||||
ReservationDetailV2,
|
|
||||||
ReservationPaymentRequest,
|
|
||||||
ReservationPaymentResponse,
|
|
||||||
ReservationRetrieveListResponse,
|
|
||||||
ReservationRetrieveResponse,
|
|
||||||
ReservationSearchQuery,
|
|
||||||
ReservationSummaryListV2,
|
|
||||||
WaitingCreateRequest
|
|
||||||
} from "./reservationTypes";
|
|
||||||
|
|
||||||
// GET /reservations
|
|
||||||
export const fetchReservations = async (): Promise<ReservationRetrieveListResponse> => {
|
|
||||||
return await apiClient.get<ReservationRetrieveListResponse>('/reservations', true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// GET /reservations-mine
|
|
||||||
export const fetchMyReservations = async (): Promise<MyReservationRetrieveListResponse> => {
|
|
||||||
return await apiClient.get<MyReservationRetrieveListResponse>('/reservations-mine', true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// GET /reservations/search
|
|
||||||
export const searchReservations = async (params: ReservationSearchQuery): Promise<ReservationRetrieveListResponse> => {
|
|
||||||
const query = new URLSearchParams();
|
|
||||||
if (params.themeId) query.append('themeId', params.themeId.toString());
|
|
||||||
if (params.memberId) query.append('memberId', params.memberId.toString());
|
|
||||||
if (params.dateFrom) query.append('dateFrom', params.dateFrom);
|
|
||||||
if (params.dateTo) query.append('dateTo', params.dateTo);
|
|
||||||
return await apiClient.get<ReservationRetrieveListResponse>(`/reservations/search?${query.toString()}`, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// DELETE /reservations/{id}
|
|
||||||
export const cancelReservationByAdmin = async (id: string): Promise<void> => {
|
|
||||||
return await apiClient.del(`/reservations/${id}`, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// POST /reservations
|
|
||||||
export const createReservationWithPayment = async (data: ReservationCreateWithPaymentRequest): Promise<ReservationRetrieveResponse> => {
|
|
||||||
return await apiClient.post<ReservationRetrieveResponse>('/reservations', data, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// POST /reservations/admin
|
|
||||||
export const createReservationByAdmin = async (data: AdminReservationCreateRequest): Promise<ReservationRetrieveResponse> => {
|
|
||||||
return await apiClient.post<ReservationRetrieveResponse>('/reservations/admin', data, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// GET /reservations/waiting
|
|
||||||
export const fetchWaitingReservations = async (): Promise<ReservationRetrieveListResponse> => {
|
|
||||||
return await apiClient.get<ReservationRetrieveListResponse>('/reservations/waiting', true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// POST /reservations/waiting
|
|
||||||
export const createWaiting = async (data: WaitingCreateRequest): Promise<ReservationRetrieveResponse> => {
|
|
||||||
return await apiClient.post<ReservationRetrieveResponse>('/reservations/waiting', data, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// DELETE /reservations/waiting/{id}
|
|
||||||
export const cancelWaiting = async (id: string): Promise<void> => {
|
|
||||||
return await apiClient.del(`/reservations/waiting/${id}`, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// POST /reservations/waiting/{id}/confirm
|
|
||||||
export const confirmWaiting = async (id: string): Promise<void> => {
|
|
||||||
return await apiClient.post(`/reservations/waiting/${id}/confirm`, {}, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// POST /reservations/waiting/{id}/reject
|
|
||||||
export const rejectWaiting = async (id: string): Promise<void> => {
|
|
||||||
return await apiClient.post(`/reservations/waiting/${id}/reject`, {}, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// POST /v2/reservations
|
|
||||||
export const createPendingReservation = async (data: ReservationCreateRequest): Promise<ReservationCreateResponse> => {
|
|
||||||
return await apiClient.post<ReservationCreateResponse>('/v2/reservations', data, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// POST /v2/reservations/{id}/pay
|
|
||||||
export const confirmReservationPayment = async (id: string, data: ReservationPaymentRequest): Promise<ReservationPaymentResponse> => {
|
|
||||||
return await apiClient.post<ReservationPaymentResponse>(`/v2/reservations/${id}/pay`, data, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// GET /v2/reservations
|
|
||||||
export const fetchMyReservationsV2 = async (): Promise<ReservationSummaryListV2> => {
|
|
||||||
return await apiClient.get<ReservationSummaryListV2>('/v2/reservations', true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// GET /v2/reservations/{id}/details
|
|
||||||
export const fetchReservationDetailV2 = async (id: string): Promise<ReservationDetailV2> => {
|
|
||||||
return await apiClient.get<ReservationDetailV2>(`/v2/reservations/${id}/details`, true);
|
|
||||||
};
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
export const ReservationStatus = {
|
|
||||||
PENDING: 'PENDING',
|
|
||||||
CONFIRMED: 'CONFIRMED',
|
|
||||||
CONFIRMED_PAYMENT_REQUIRED: 'CONFIRMED_PAYMENT_REQUIRED',
|
|
||||||
WAITING: 'WAITING',
|
|
||||||
CANCELED_BY_USER: 'CANCELED_BY_USER',
|
|
||||||
AUTOMATICALLY_CANCELED: 'AUTOMATICALLY_CANCELED'
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type ReservationStatus =
|
|
||||||
| typeof ReservationStatus.PENDING
|
|
||||||
| typeof ReservationStatus.CONFIRMED
|
|
||||||
| typeof ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
|
||||||
| typeof ReservationStatus.WAITING
|
|
||||||
| typeof ReservationStatus.CANCELED_BY_USER
|
|
||||||
| typeof ReservationStatus.AUTOMATICALLY_CANCELED;
|
|
||||||
|
|
||||||
export interface MyReservationRetrieveResponse {
|
|
||||||
id: string;
|
|
||||||
themeName: string;
|
|
||||||
date: string;
|
|
||||||
time: string;
|
|
||||||
status: ReservationStatus;
|
|
||||||
rank: number;
|
|
||||||
paymentKey: string | null;
|
|
||||||
amount: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MyReservationRetrieveListResponse {
|
|
||||||
reservations: MyReservationRetrieveResponse[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReservationRetrieveResponse {
|
|
||||||
id: string;
|
|
||||||
date: string;
|
|
||||||
member: MemberRetrieveResponse;
|
|
||||||
time: TimeRetrieveResponse;
|
|
||||||
theme: ThemeRetrieveResponse;
|
|
||||||
status: ReservationStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReservationRetrieveListResponse {
|
|
||||||
reservations: ReservationRetrieveResponse[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminReservationCreateRequest {
|
|
||||||
date: string;
|
|
||||||
timeId: string;
|
|
||||||
themeId: string;
|
|
||||||
memberId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReservationCreateWithPaymentRequest {
|
|
||||||
date: string;
|
|
||||||
timeId: string;
|
|
||||||
themeId: string;
|
|
||||||
paymentKey: string;
|
|
||||||
orderId: string;
|
|
||||||
amount: number;
|
|
||||||
paymentType: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WaitingCreateRequest {
|
|
||||||
date: string;
|
|
||||||
timeId: string;
|
|
||||||
themeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReservationSearchQuery {
|
|
||||||
themeId?: string;
|
|
||||||
memberId?: string;
|
|
||||||
dateFrom?: string;
|
|
||||||
dateTo?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PaymentStatus = {
|
|
||||||
IN_PROGRESS: '결제 진행 중',
|
|
||||||
DONE: '결제 완료',
|
|
||||||
CANCELED: '결제 취소',
|
|
||||||
ABORTED: '결제 중단',
|
|
||||||
EXPIRED: '시간 만료',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PaymentStatus =
|
|
||||||
| typeof PaymentStatus.IN_PROGRESS
|
|
||||||
| typeof PaymentStatus.DONE
|
|
||||||
| typeof PaymentStatus.CANCELED
|
|
||||||
| typeof PaymentStatus.ABORTED
|
|
||||||
| typeof PaymentStatus.EXPIRED;
|
|
||||||
|
|
||||||
|
|
||||||
export interface ReservationCreateRequest {
|
|
||||||
date: string;
|
|
||||||
timeId: string;
|
|
||||||
themeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReservationCreateResponse {
|
|
||||||
reservationId: string;
|
|
||||||
memberEmail: string;
|
|
||||||
date: string;
|
|
||||||
startAt: string;
|
|
||||||
themeName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReservationPaymentRequest {
|
|
||||||
paymentKey: string;
|
|
||||||
orderId: string;
|
|
||||||
amount: number;
|
|
||||||
paymentType: PaymentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReservationPaymentResponse {
|
|
||||||
reservationId: string;
|
|
||||||
reservationStatus: ReservationStatus;
|
|
||||||
paymentId: string;
|
|
||||||
paymentStatus: PaymentStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface ReservationDetailV2 {
|
|
||||||
id: string;
|
|
||||||
user: MemberSummaryRetrieveResponse;
|
|
||||||
themeName: string;
|
|
||||||
date: string;
|
|
||||||
startAt: string;
|
|
||||||
applicationDateTime: string;
|
|
||||||
payment: PaymentRetrieveResponse;
|
|
||||||
}
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import type {MemberSummaryRetrieveResponse} from "@_api/member/memberTypes";
|
import type { PaymentRetrieveResponse } from "@_api/payment/PaymentTypes";
|
||||||
import type {PaymentRetrieveResponse} from "@_api/payment/PaymentTypes";
|
import type { UserContactRetrieveResponse } from "@_api/user/userTypes";
|
||||||
|
|
||||||
export const ReservationStatusV2 = {
|
export const ReservationStatusV2 = {
|
||||||
PENDING: 'PENDING',
|
PENDING: 'PENDING',
|
||||||
@ -42,7 +42,7 @@ export interface ReservationSummaryRetrieveListResponse {
|
|||||||
|
|
||||||
export interface ReservationDetailRetrieveResponse {
|
export interface ReservationDetailRetrieveResponse {
|
||||||
id: string;
|
id: string;
|
||||||
member: MemberSummaryRetrieveResponse;
|
user: UserContactRetrieveResponse;
|
||||||
applicationDateTime: string;
|
applicationDateTime: string;
|
||||||
payment: PaymentRetrieveResponse;
|
payment: PaymentRetrieveResponse;
|
||||||
}
|
}
|
||||||
@ -52,7 +52,7 @@ export interface ReservationDetail {
|
|||||||
themeName: string;
|
themeName: string;
|
||||||
date: string;
|
date: string;
|
||||||
startAt: string;
|
startAt: string;
|
||||||
member: MemberSummaryRetrieveResponse;
|
user: UserContactRetrieveResponse;
|
||||||
applicationDateTime: string;
|
applicationDateTime: string;
|
||||||
payment: PaymentRetrieveResponse;
|
payment: PaymentRetrieveResponse;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -147,9 +147,16 @@ export interface ThemeRetrieveListResponseV2 {
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export enum Difficulty {
|
export enum Difficulty {
|
||||||
VERY_EASY = 'VERY_EASY',
|
VERY_EASY = '매우 쉬움',
|
||||||
EASY = 'EASY',
|
EASY = '쉬움',
|
||||||
NORMAL = 'NORMAL',
|
NORMAL = '보통',
|
||||||
HARD = 'HARD',
|
HARD = '어려움',
|
||||||
VERY_HARD = 'VERY_HARD',
|
VERY_HARD = '매우 어려움',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapThemeResponse(res: any): UserThemeRetrieveResponse {
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
difficulty: Difficulty[res.difficulty as keyof typeof Difficulty],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import apiClient from "@_api/apiClient";
|
|
||||||
import type { TimeCreateRequest, TimeCreateResponse, TimeRetrieveListResponse, TimeWithAvailabilityListResponse } from "./timeTypes";
|
|
||||||
|
|
||||||
export const createTime = async (data: TimeCreateRequest): Promise<TimeCreateResponse> => {
|
|
||||||
return await apiClient.post<TimeCreateResponse>('/times', data, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetchTimes = async (): Promise<TimeRetrieveListResponse> => {
|
|
||||||
return await apiClient.get<TimeRetrieveListResponse>('/times', true);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const delTime = async (id: string): Promise<void> => {
|
|
||||||
return await apiClient.del(`/times/${id}`, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchTimesWithAvailability = async (date: string, themeId: string): Promise<TimeWithAvailabilityListResponse> => {
|
|
||||||
return await apiClient.get<TimeWithAvailabilityListResponse>(`/times/search?date=${date}&themeId=${themeId}`, true);
|
|
||||||
};
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
export interface TimeCreateRequest {
|
|
||||||
startAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimeCreateResponse {
|
|
||||||
id: string;
|
|
||||||
startAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimeRetrieveResponse {
|
|
||||||
id: string;
|
|
||||||
startAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimeRetrieveListResponse {
|
|
||||||
times: TimeCreateResponse[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimeWithAvailabilityResponse {
|
|
||||||
id: string;
|
|
||||||
startAt: string;
|
|
||||||
isAvailable: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimeWithAvailabilityListResponse {
|
|
||||||
times: TimeWithAvailabilityResponse[];
|
|
||||||
}
|
|
||||||
@ -1,6 +1,10 @@
|
|||||||
import apiClient from "@_api/apiClient";
|
import apiClient from "@_api/apiClient";
|
||||||
import type { 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, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchContact = async (): Promise<UserContactRetrieveResponse> => {
|
||||||
|
return await apiClient.get<UserContactRetrieveResponse>('/users/contact', true);
|
||||||
|
}
|
||||||
|
|||||||
@ -16,12 +16,17 @@ export interface UserCreateRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UserCreateResponse {
|
export interface UserCreateResponse {
|
||||||
id: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserContactRetrieveResponse {
|
export interface UserContactRetrieveResponse {
|
||||||
id: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OperatorInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|||||||
@ -21,20 +21,20 @@ const Navbar: React.FC = () => {
|
|||||||
<nav className="navbar-container">
|
<nav className="navbar-container">
|
||||||
<div className="nav-links">
|
<div className="nav-links">
|
||||||
<Link className="nav-link" to="/">홈</Link>
|
<Link className="nav-link" to="/">홈</Link>
|
||||||
<Link className="nav-link" to="/v2/reservation">예약하기</Link>
|
<Link className="nav-link" to="/reservation">예약하기</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="nav-actions">
|
<div className="nav-actions">
|
||||||
{!loggedIn ? (
|
{!loggedIn ? (
|
||||||
<>
|
<>
|
||||||
<button className="btn btn-secondary" onClick={() => navigate('/v2/login')}>로그인</button>
|
<button className="btn btn-secondary" onClick={() => navigate('/login')}>로그인</button>
|
||||||
<button className="btn btn-primary" onClick={() => navigate('/v2/signup')}>회원가입</button>
|
<button className="btn btn-primary" onClick={() => navigate('/signup')}>회원가입</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="profile-info">
|
<div className="profile-info">
|
||||||
<img className="profile-image" src="/image/default-profile.png" alt="Profile" />
|
<img className="profile-image" src="/image/default-profile.png" alt="Profile" />
|
||||||
<span>{userName}</span>
|
<span>{userName}</span>
|
||||||
<div className="dropdown-menu">
|
<div className="dropdown-menu">
|
||||||
<Link className="dropdown-item" to="/my-reservation/v2">내 예약</Link>
|
<Link className="dropdown-item" to="/my-reservation">내 예약</Link>
|
||||||
<div className="dropdown-divider" />
|
<div className="dropdown-divider" />
|
||||||
<a className="dropdown-item" href="#" onClick={handleLogout}>로그아웃</a>
|
<a className="dropdown-item" href="#" onClick={handleLogout}>로그아웃</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { checkLogin as apiCheckLogin, login as apiLogin, logout as apiLogout } from '@_api/auth/authAPI';
|
import { checkLogin as apiCheckLogin, login as apiLogin, logout as apiLogout } from '@_api/auth/authAPI';
|
||||||
import type { LoginRequest, LoginResponse } from '@_api/auth/authTypes';
|
import { PrincipalType, type LoginRequest, type LoginSuccessResponse } from '@_api/auth/authTypes';
|
||||||
import React, { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
|
import React, { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
loggedIn: boolean;
|
loggedIn: boolean;
|
||||||
userName: string | null;
|
userName: string | null;
|
||||||
role: 'ADMIN' | 'MEMBER' | null;
|
type: PrincipalType | null;
|
||||||
loading: boolean; // Add loading state to type
|
loading: boolean;
|
||||||
login: (data: LoginRequest) => Promise<LoginResponse>;
|
login: (data: LoginRequest) => Promise<LoginSuccessResponse>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
checkLogin: () => Promise<void>;
|
checkLogin: () => Promise<void>;
|
||||||
}
|
}
|
||||||
@ -17,7 +17,7 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|||||||
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
const [loggedIn, setLoggedIn] = useState(false);
|
const [loggedIn, setLoggedIn] = useState(false);
|
||||||
const [userName, setUserName] = useState<string | null>(null);
|
const [userName, setUserName] = useState<string | null>(null);
|
||||||
const [role, setRole] = useState<'ADMIN' | 'MEMBER' | null>(null);
|
const [type, setType] = useState<PrincipalType | null>(null);
|
||||||
const [loading, setLoading] = useState(true); // Add loading state
|
const [loading, setLoading] = useState(true); // Add loading state
|
||||||
|
|
||||||
const checkLogin = async () => {
|
const checkLogin = async () => {
|
||||||
@ -25,11 +25,11 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
const response = await apiCheckLogin();
|
const response = await apiCheckLogin();
|
||||||
setLoggedIn(true);
|
setLoggedIn(true);
|
||||||
setUserName(response.name);
|
setUserName(response.name);
|
||||||
setRole(response.role);
|
setType(response.type);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoggedIn(false);
|
setLoggedIn(false);
|
||||||
setUserName(null);
|
setUserName(null);
|
||||||
setRole(null);
|
setType(null);
|
||||||
localStorage.removeItem('accessToken');
|
localStorage.removeItem('accessToken');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false); // Set loading to false after check is complete
|
setLoading(false); // Set loading to false after check is complete
|
||||||
@ -41,8 +41,9 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const login = async (data: LoginRequest) => {
|
const login = async (data: LoginRequest) => {
|
||||||
const response = await apiLogin(data);
|
const response = await apiLogin({ ...data });
|
||||||
await checkLogin();
|
setLoggedIn(true);
|
||||||
|
setType(data.principalType);
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,13 +53,13 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
} finally {
|
} finally {
|
||||||
setLoggedIn(false);
|
setLoggedIn(false);
|
||||||
setUserName(null);
|
setUserName(null);
|
||||||
setRole(null);
|
setType(null);
|
||||||
localStorage.removeItem('accessToken');
|
localStorage.removeItem('accessToken');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ loggedIn, userName, role, loading, login, logout, checkLogin }}>
|
<AuthContext.Provider value={{ loggedIn, userName, type, loading, login, logout, checkLogin }}>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -68,17 +68,77 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Canceled Card Style */
|
/* --- Status Badge --- */
|
||||||
.reservation-summary-card-v2.status-canceled_by_user {
|
.card-status-badge {
|
||||||
background-color: #f8f9fa;
|
position: absolute;
|
||||||
opacity: 0.6;
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reservation-summary-card-v2.status-canceled_by_user .summary-theme-name-v2,
|
/* --- Card Status Styles --- */
|
||||||
.reservation-summary-card-v2.status-canceled_by_user .summary-datetime-v2,
|
.reservation-summary-card-v2 {
|
||||||
.reservation-summary-card-v2.status-canceled_by_user .summary-details-v2 strong {
|
position: relative; /* For badge positioning */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Confirmed (Upcoming) */
|
||||||
|
.reservation-summary-card-v2.status-confirmed {
|
||||||
|
border-left: 5px solid #28a745; /* Green accent */
|
||||||
|
}
|
||||||
|
.status-confirmed .card-status-badge {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Completed (Past) */
|
||||||
|
.reservation-summary-card-v2.status-completed {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-left: 5px solid #6c757d; /* Gray accent */
|
||||||
|
}
|
||||||
|
.reservation-summary-card-v2.status-completed .summary-theme-name-v2,
|
||||||
|
.reservation-summary-card-v2.status-completed .summary-datetime-v2 {
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
}
|
}
|
||||||
|
.reservation-summary-card-v2.status-completed .detail-button-v2 {
|
||||||
|
background-color: #6c757d;
|
||||||
|
}
|
||||||
|
.status-completed .card-status-badge {
|
||||||
|
background-color: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Canceled */
|
||||||
|
.reservation-summary-card-v2.status-canceled {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-left: 5px solid #dc3545; /* Red accent */
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.reservation-summary-card-v2.status-canceled .summary-theme-name-v2,
|
||||||
|
.reservation-summary-card-v2.status-canceled .summary-datetime-v2 {
|
||||||
|
color: #6c757d;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
.reservation-summary-card-v2.status-canceled .detail-button-v2 {
|
||||||
|
background-color: #6c757d;
|
||||||
|
}
|
||||||
|
.status-canceled .card-status-badge {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pending */
|
||||||
|
.reservation-summary-card-v2.status-pending {
|
||||||
|
border-left: 5px solid #ffc107; /* Yellow accent */
|
||||||
|
}
|
||||||
|
.status-pending .card-status-badge {
|
||||||
|
background-color: #ffc107;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
/* Detail Button */
|
/* Detail Button */
|
||||||
.detail-button-v2 {
|
.detail-button-v2 {
|
||||||
|
|||||||
@ -1,31 +1,90 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import {fetchMostReservedThemeIds} from '@_api/reservation/reservationAPIV2';
|
||||||
import { mostReservedThemes } from '@_api/theme/themeAPI';
|
import '@_css/home-page-v2.css';
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import {useNavigate} from 'react-router-dom';
|
||||||
|
import {findThemesByIds} from '@_api/theme/themeAPI';
|
||||||
|
import {type UserThemeRetrieveResponse} from '@_api/theme/themeTypes';
|
||||||
|
|
||||||
const HomePage: React.FC = () => {
|
const HomePage: React.FC = () => {
|
||||||
const [ranking, setRanking] = useState<any[]>([]);
|
const [ranking, setRanking] = useState<UserThemeRetrieveResponse[]>([]);
|
||||||
|
const [selectedTheme, setSelectedTheme] = useState<UserThemeRetrieveResponse | null>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
await mostReservedThemes(10).then(response => setRanking(response.themes))
|
try {
|
||||||
|
const themeIds = await fetchMostReservedThemeIds().then(res => {
|
||||||
|
const themeIds = res.themeIds;
|
||||||
|
if (themeIds.length === 0) {
|
||||||
|
setRanking([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return themeIds;
|
||||||
|
})
|
||||||
|
|
||||||
|
if (themeIds === undefined) return;
|
||||||
|
if (themeIds.length === 0) return;
|
||||||
|
|
||||||
|
const response = await findThemesByIds({ themeIds: themeIds });
|
||||||
|
setRanking(response.themes);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching ranking:', err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData().catch(err => console.error('Error fetching ranking:', err));
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleThemeClick = (theme: UserThemeRetrieveResponse) => {
|
||||||
|
setSelectedTheme(theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setSelectedTheme(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReservationClick = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (selectedTheme) {
|
||||||
|
navigate('/reservation', { state: { themeId: selectedTheme.id } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content-container">
|
<div className="home-container-v2">
|
||||||
<h2 className="content-container-title">인기 테마</h2>
|
<h2 className="page-title">인기 테마</h2>
|
||||||
<ul className="list-unstyled" id="theme-ranking">
|
<div className="theme-ranking-list-v2">
|
||||||
{ranking.map(theme => (
|
{ranking.map(theme => (
|
||||||
<li key={theme.id} className="d-flex my-4">
|
<div key={theme.id} className="theme-ranking-item-v2" onClick={() => handleThemeClick(theme)}>
|
||||||
<img className="me-3 img-thumbnail" src={theme.thumbnail} alt={theme.name} style={{ width: '150px' }} />
|
<img className="thumbnail" src={theme.thumbnailUrl} alt={theme.name} />
|
||||||
<div className="media-body">
|
<div className="theme-info">
|
||||||
<h5 className="mt-0 mb-1">{theme.name}</h5>
|
<h5 className="theme-name">{theme.name}</h5>
|
||||||
{theme.description}
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</div>
|
||||||
|
|
||||||
|
{selectedTheme && (
|
||||||
|
<div className="theme-modal-overlay" onClick={handleCloseModal}>
|
||||||
|
<div className="theme-modal-content" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<img className="modal-thumbnail" src={selectedTheme.thumbnailUrl} alt={selectedTheme.name} />
|
||||||
|
<div className="modal-theme-info">
|
||||||
|
<h2>{selectedTheme.name}</h2>
|
||||||
|
<p>{selectedTheme.description}</p>
|
||||||
|
<div className="theme-details">
|
||||||
|
<p><strong>난이도:</strong> {selectedTheme.difficulty}</p>
|
||||||
|
<p><strong>가격:</strong> {selectedTheme.price.toLocaleString()}원</p>
|
||||||
|
<p><strong>예상 시간:</strong> {selectedTheme.expectedMinutesFrom} ~ {selectedTheme.expectedMinutesTo}분</p>
|
||||||
|
<p><strong>이용 가능 인원:</strong> {selectedTheme.minParticipants} ~ {selectedTheme.maxParticipants}명</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="modal-buttons">
|
||||||
|
<button onClick={handleReservationClick} className="modal-button reserve">예약하기</button>
|
||||||
|
<button onClick={handleCloseModal} className="modal-button close">닫기</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '@_context/AuthContext';
|
||||||
|
import '@_css/login-page-v2.css';
|
||||||
|
|
||||||
const LoginPage: React.FC = () => {
|
const LoginPage: React.FC = () => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
@ -11,9 +12,11 @@ const LoginPage: React.FC = () => {
|
|||||||
|
|
||||||
const from = location.state?.from?.pathname || '/';
|
const from = location.state?.from?.pathname || '/';
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
await login({email, password});
|
const principalType = from.startsWith('/admin') ? 'ADMIN' : 'USER';
|
||||||
|
await login({ account: email, password: password, principalType: principalType });
|
||||||
|
|
||||||
alert('로그인에 성공했어요!');
|
alert('로그인에 성공했어요!');
|
||||||
navigate(from, { replace: true });
|
navigate(from, { replace: true });
|
||||||
@ -21,24 +24,39 @@ const LoginPage: React.FC = () => {
|
|||||||
const message = error.response?.data?.message || '로그인에 실패했어요. 이메일과 비밀번호를 확인해주세요.';
|
const message = error.response?.data?.message || '로그인에 실패했어요. 이메일과 비밀번호를 확인해주세요.';
|
||||||
alert(message);
|
alert(message);
|
||||||
console.error('로그인 실패:', error);
|
console.error('로그인 실패:', error);
|
||||||
setEmail('');
|
|
||||||
setPassword('');
|
setPassword('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content-container" style={{ width: '300px' }}>
|
<div className="login-container-v2">
|
||||||
<h2 className="content-container-title">Login</h2>
|
<h2 className="page-title">로그인</h2>
|
||||||
<div className="form-group">
|
<form className="login-form-v2" onSubmit={handleLogin}>
|
||||||
<input type="email" className="form-control" placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} />
|
<div className="form-group">
|
||||||
</div>
|
<input
|
||||||
<div className="form-group">
|
type="email"
|
||||||
<input type="password" className="form-control" placeholder="Password" value={password} onChange={e => setPassword(e.target.value)} />
|
className="form-input"
|
||||||
</div>
|
placeholder="이메일"
|
||||||
<div className="button-group full-width">
|
value={email}
|
||||||
<button className="btn btn-outline-custom" onClick={() => navigate('/signup')}>Sign Up</button>
|
onChange={e => setEmail(e.target.value)}
|
||||||
<button className="btn btn-custom" onClick={handleLogin}>Login</button>
|
required
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="비밀번호"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="button-group">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => navigate('/signup')}>회원가입</button>
|
||||||
|
<button type="submit" className="btn btn-primary">로그인</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,88 +1,378 @@
|
|||||||
|
import { cancelPayment } from '@_api/payment/paymentAPI';
|
||||||
|
import type { PaymentRetrieveResponse } from '@_api/payment/PaymentTypes';
|
||||||
|
import { cancelReservation, fetchDetailById, fetchSummaryByMember } from '@_api/reservation/reservationAPIV2';
|
||||||
|
import { ReservationStatusV2, type ReservationDetail, type ReservationSummaryRetrieveResponse } from '@_api/reservation/reservationTypesV2';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import '@_css/my-reservation-v2.css';
|
||||||
import { cancelWaiting, fetchMyReservations } from '@_api/reservation/reservationAPI';
|
|
||||||
import type { MyReservationRetrieveResponse } from '@_api/reservation/reservationTypes';
|
|
||||||
import { ReservationStatus } from '@_api/reservation/reservationTypes';
|
|
||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
|
||||||
|
|
||||||
|
const getReservationStatus = (reservation: ReservationSummaryRetrieveResponse): { className: string, text: string } => {
|
||||||
|
const now = new Date();
|
||||||
|
const reservationDateTime = new Date(`${reservation.date}T${reservation.startAt}`);
|
||||||
|
|
||||||
|
switch (reservation.status) {
|
||||||
|
case ReservationStatusV2.CANCELED:
|
||||||
|
return { className: 'status-canceled', text: '취소됨' };
|
||||||
|
case ReservationStatusV2.CONFIRMED:
|
||||||
|
if (reservationDateTime < now) {
|
||||||
|
return { className: 'status-completed', text: '이용완료' };
|
||||||
|
}
|
||||||
|
return { className: 'status-confirmed', text: '예약확정' };
|
||||||
|
case ReservationStatusV2.PENDING:
|
||||||
|
return { className: 'status-pending', text: '입금대기' };
|
||||||
|
default:
|
||||||
|
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 ---
|
||||||
|
const CancellationView: React.FC<{
|
||||||
|
reservation: ReservationDetail;
|
||||||
|
onCancelSubmit: (reason: string) => void;
|
||||||
|
onBack: () => void;
|
||||||
|
isCancelling: boolean;
|
||||||
|
}> = ({ reservation, onCancelSubmit, onBack, isCancelling }) => {
|
||||||
|
const [reason, setReason] = useState('');
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!reason.trim()) {
|
||||||
|
alert('취소 사유를 입력해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onCancelSubmit(reason);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="cancellation-view-v2">
|
||||||
|
<h3>취소 정보 확인</h3>
|
||||||
|
<div className="cancellation-summary-v2">
|
||||||
|
<p><strong>테마:</strong> {reservation.themeName}</p>
|
||||||
|
<p><strong>신청 일시:</strong> {formatDisplayDateTime(reservation.applicationDateTime)}</p>
|
||||||
|
<p><strong>결제 금액:</strong> {reservation.payment.totalAmount.toLocaleString()}원</p>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={reason}
|
||||||
|
onChange={(e) => setReason(e.target.value)}
|
||||||
|
placeholder="취소 사유를 입력해주세요."
|
||||||
|
className="cancellation-reason-textarea-v2"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
<div className="modal-actions-v2">
|
||||||
|
<button onClick={onBack} className="back-button-v2" disabled={isCancelling}>뒤로가기</button>
|
||||||
|
<button onClick={handleSubmit} className="cancel-submit-button-v2" disabled={isCancelling}>
|
||||||
|
{isCancelling ? '취소 중...' : '취소 요청'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const ReservationDetailView: React.FC<{
|
||||||
|
reservation: ReservationDetail;
|
||||||
|
onGoToCancel: () => void;
|
||||||
|
}> = ({ reservation, onGoToCancel }) => {
|
||||||
|
|
||||||
|
const renderPaymentSubDetails = (payment: PaymentRetrieveResponse) => {
|
||||||
|
if (!payment.detail) {
|
||||||
|
return <p>결제 상세 정보를 찾을 수 없어요. 관리자에게 문의해주세요.</p>;
|
||||||
|
}
|
||||||
|
const { detail } = payment;
|
||||||
|
|
||||||
|
switch (detail.type) {
|
||||||
|
case 'CARD':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{payment.totalAmount !== detail.amount && (
|
||||||
|
<>
|
||||||
|
<p><strong>(카드)승인 금액:</strong> {detail.amount.toLocaleString()}원</p>
|
||||||
|
{detail.easypayDiscountAmount && (
|
||||||
|
<p><strong>(간편결제)할인 금액:</strong> {detail.easypayDiscountAmount.toLocaleString()}원</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{detail.easypayProviderName && (
|
||||||
|
<p><strong>간편결제사: </strong> {detail.easypayProviderName}</p>
|
||||||
|
)}
|
||||||
|
<p><strong>카드사 / 구분:</strong> {detail.issuerCode}({detail.ownerType}) / {detail.cardType}</p>
|
||||||
|
<p><strong>카드 번호:</strong> {detail.cardNumber}</p>
|
||||||
|
<p><strong>할부 방식:</strong> {detail.installmentPlanMonths === 0 ? '일시불' : `${detail.installmentPlanMonths}개월`}</p>
|
||||||
|
<p><strong>승인 번호:</strong> {detail.approvalNumber}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case 'BANK_TRANSFER':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p><strong>은행:</strong> {detail.bankName}</p>
|
||||||
|
<p><strong>정산 상태:</strong> {detail.settlementStatus}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case 'EASYPAY_PREPAID':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p><strong>결제 금액:</strong> {detail.amount.toLocaleString()}원</p>
|
||||||
|
{detail.discountAmount > 0 && <p><strong>포인트 사용:</strong> {detail.discountAmount.toLocaleString()}원</p>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <p>상세 결제 수단 정보를 표시할 수 없습니다.</p>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="modal-section-v2">
|
||||||
|
<h3>예약 정보</h3>
|
||||||
|
<p><strong>예약 테마:</strong> {reservation.themeName}</p>
|
||||||
|
<p><strong>이용 예정일:</strong> {formatCardDateTime(reservation.date, reservation.startAt)}</p>
|
||||||
|
<p><strong>예약자 이름:</strong> {reservation.user.name}</p>
|
||||||
|
<p><strong>예약자 이메일:</strong> {reservation.user.phone}</p>
|
||||||
|
<p><strong>예약 신청 일시:</strong> {formatDisplayDateTime(reservation.applicationDateTime)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!reservation.payment ? (
|
||||||
|
<div className="modal-section-v2">
|
||||||
|
<h3>결제 정보</h3>
|
||||||
|
<p>결제 정보를 찾을 수 없어요.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="modal-section-v2">
|
||||||
|
<h3>결제 정보</h3>
|
||||||
|
<p><strong>주문 ID:</strong> {reservation.payment.orderId}</p>
|
||||||
|
<p><strong>총 결제액:</strong> {reservation.payment.totalAmount.toLocaleString()}원</p>
|
||||||
|
<p><strong>결제 수단:</strong> {reservation.payment.method}</p>
|
||||||
|
{reservation.payment.approvedAt && <p><strong>결제 승인 일시:</strong> {formatDisplayDateTime(reservation.payment.approvedAt)}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="modal-section-v2">
|
||||||
|
<h3>결제 상세 정보</h3>
|
||||||
|
{renderPaymentSubDetails(reservation.payment)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{reservation.payment && reservation.payment.cancel && (
|
||||||
|
<div className="modal-section-v2 cancellation-section-v2">
|
||||||
|
<h3>취소 정보</h3>
|
||||||
|
<p><strong>취소 요청 일시:</strong> {formatDisplayDateTime(reservation.payment.cancel.cancellationRequestedAt)}</p>
|
||||||
|
<p><strong>환불 완료 일시:</strong> {formatDisplayDateTime(reservation.payment.cancel.cancellationApprovedAt)}</p>
|
||||||
|
<p><strong>취소 사유:</strong> {reservation.payment.cancel.cancelReason}</p>
|
||||||
|
<p><strong>취소 요청인:</strong> {reservation.payment.cancel.canceledBy == reservation.user.id ? '회원 본인' : '관리자'}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{reservation.payment && reservation.payment.status !== 'CANCELED' && (
|
||||||
|
<div className="modal-actions-v2">
|
||||||
|
<button onClick={onGoToCancel} className="cancel-button-v2">예약 취소하기</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Main Page Component ---
|
||||||
const MyReservationPage: React.FC = () => {
|
const MyReservationPage: React.FC = () => {
|
||||||
const [reservations, setReservations] = useState<MyReservationRetrieveResponse[]>([]);
|
const [reservations, setReservations] = useState<ReservationSummaryRetrieveResponse[]>([]);
|
||||||
const navigate = useNavigate();
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleError = (err: any) => {
|
const [selectedReservation, setSelectedReservation] = useState<ReservationDetail | null>(null);
|
||||||
if (isLoginRequiredError(err)) {
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
alert('로그인이 필요해요.');
|
const [isDetailLoading, setIsDetailLoading] = useState(false);
|
||||||
navigate('/login', { state: { from: location } });
|
const [detailError, setDetailError] = useState<string | null>(null);
|
||||||
} else {
|
|
||||||
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
|
const [modalView, setModalView] = useState<'detail' | 'cancel'>('detail');
|
||||||
alert(message);
|
const [isCancelling, setIsCancelling] = useState(false);
|
||||||
console.error(err);
|
|
||||||
|
const loadReservations = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const data = await fetchSummaryByMember();
|
||||||
|
setReservations(data.reservations);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError('예약 목록을 불러오는 데 실패했습니다.');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMyReservations()
|
loadReservations();
|
||||||
.then(res => setReservations(res.reservations))
|
|
||||||
.catch(handleError);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const _cancelWaiting = (id: string) => {
|
const handleShowDetail = async (id: string, themeName: string, date: string, time: string) => {
|
||||||
cancelWaiting(id)
|
try {
|
||||||
.then(() => {
|
setIsDetailLoading(true);
|
||||||
alert('예약 대기가 취소되었습니다.');
|
setDetailError(null);
|
||||||
setReservations(reservations.filter(r => r.id.toString() !== id));
|
setModalView('detail');
|
||||||
})
|
const detailData = await fetchDetailById(id);
|
||||||
.catch(handleError);
|
setSelectedReservation({
|
||||||
|
id: detailData.id,
|
||||||
|
themeName: themeName,
|
||||||
|
date: date,
|
||||||
|
startAt: time,
|
||||||
|
user: detailData.user,
|
||||||
|
applicationDateTime: detailData.applicationDateTime,
|
||||||
|
payment: detailData.payment
|
||||||
|
});
|
||||||
|
setIsModalOpen(true);
|
||||||
|
} catch (err) {
|
||||||
|
setDetailError('예약 상세 정보를 불러오는 데 실패했습니다.');
|
||||||
|
} finally {
|
||||||
|
setIsDetailLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusText = (status: ReservationStatus, rank: number) => {
|
const handleCloseModal = () => {
|
||||||
if (status === ReservationStatus.CONFIRMED) {
|
setIsModalOpen(false);
|
||||||
return '예약';
|
setSelectedReservation(null);
|
||||||
}
|
|
||||||
if (status === ReservationStatus.CONFIRMED_PAYMENT_REQUIRED) {
|
|
||||||
return '예약 - 결제 필요';
|
|
||||||
}
|
|
||||||
if (status === ReservationStatus.WAITING) {
|
|
||||||
return `${rank}번째 예약 대기`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCancelSubmit = async (reason: string) => {
|
||||||
|
if (!selectedReservation) return;
|
||||||
|
|
||||||
|
if (!window.confirm('정말 취소하시겠어요?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsCancelling(true);
|
||||||
|
setDetailError(null);
|
||||||
|
await cancelReservation(selectedReservation.id, reason);
|
||||||
|
cancelPayment({ reservationId: selectedReservation.id, cancelReason: reason });
|
||||||
|
alert('예약을 취소했어요. 결제 취소까지는 3-5일 정도 소요될 수 있어요.');
|
||||||
|
handleCloseModal();
|
||||||
|
await loadReservations(); // Refresh the list
|
||||||
|
} catch (err) {
|
||||||
|
setDetailError(err instanceof Error ? err.message : '예약 취소에 실패했습니다.');
|
||||||
|
} finally {
|
||||||
|
setIsCancelling(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
console.log("reservations=", reservations);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content-container">
|
<div className="my-reservation-container-v2">
|
||||||
<h2 className="content-container-title">내 예약</h2>
|
<h1>내 예약 V2</h1>
|
||||||
<div className="table-container"></div>
|
|
||||||
<table className="table">
|
{isLoading && <p>목록 로딩 중...</p>}
|
||||||
<thead>
|
{error && <p className="error-message-v2">{error}</p>}
|
||||||
<tr>
|
|
||||||
<th>테마</th>
|
{!isLoading && !error && (
|
||||||
<th>날짜</th>
|
<div className="reservation-list-v2">
|
||||||
<th>시간</th>
|
{reservations.map((res) => {
|
||||||
<th>상태</th>
|
const status = getReservationStatus(res);
|
||||||
<th>대기 취소</th>
|
return (
|
||||||
<th>paymentKey</th>
|
<div key={res.id} className={`reservation-summary-card-v2 ${status.className}`}>
|
||||||
<th>결제금액</th>
|
<div className="card-status-badge">{status.text}</div>
|
||||||
<th></th>
|
<div className="summary-details-v2">
|
||||||
</tr>
|
<h3 className="summary-theme-name-v2">{res.themeName}</h3>
|
||||||
</thead>
|
<p className="summary-datetime-v2">{formatCardDateTime(res.date, res.startAt)}</p>
|
||||||
<tbody>
|
</div>
|
||||||
{reservations.map(r => (
|
<button
|
||||||
<tr key={r.id}>
|
onClick={() => handleShowDetail(res.id, res.themeName, res.date, res.startAt)}
|
||||||
<td>{r.themeName}</td>
|
disabled={isDetailLoading}
|
||||||
<td>{r.date}</td>
|
className="detail-button-v2"
|
||||||
<td>{r.time}</td>
|
>
|
||||||
<td>{getStatusText(r.status, r.rank)}</td>
|
{isDetailLoading && selectedReservation?.id === res.id ? '로딩중...' : '상세보기'}
|
||||||
<td>
|
</button>
|
||||||
{r.status === ReservationStatus.WAITING &&
|
</div>
|
||||||
<button className="btn btn-danger" onClick={() => _cancelWaiting(r.id.toString())}>취소</button>}
|
);
|
||||||
</td>
|
})}
|
||||||
<td>{r.paymentKey}</td>
|
</div>
|
||||||
<td>{r.amount}</td>
|
)}
|
||||||
<td></td>
|
|
||||||
</tr>
|
{isModalOpen && selectedReservation && (
|
||||||
))}
|
<div className="modal-overlay-v2" onClick={handleCloseModal}>
|
||||||
</tbody>
|
<div className="modal-content-v2" onClick={(e) => e.stopPropagation()}>
|
||||||
</table>
|
<button className="modal-close-button-v2" onClick={handleCloseModal}>×</button>
|
||||||
|
<h2>{modalView === 'detail' ? '예약 상세 정보' : '예약 취소'}</h2>
|
||||||
|
{detailError && <p className="error-message-v2">{detailError}</p>}
|
||||||
|
|
||||||
|
{modalView === 'detail' ? (
|
||||||
|
<ReservationDetailView
|
||||||
|
reservation={selectedReservation}
|
||||||
|
onGoToCancel={() => setModalView('cancel')}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CancellationView
|
||||||
|
reservation={selectedReservation}
|
||||||
|
onCancelSubmit={handleCancelSubmit}
|
||||||
|
onBack={() => setModalView('detail')}
|
||||||
|
isCancelling={isCancelling}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
import { isLoginRequiredError } from '@_api/apiClient';
|
||||||
import { createPendingReservation } from '@_api/reservation/reservationAPIV2';
|
import { createPendingReservation } from '@_api/reservation/reservationAPIV2';
|
||||||
|
import { fetchContact } from '@_api/user/userAPI';
|
||||||
import '@_css/reservation-v2-1.css';
|
import '@_css/reservation-v2-1.css';
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
|
import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
|
||||||
|
|
||||||
@ -12,8 +13,25 @@ const ReservationFormPage: React.FC = () => {
|
|||||||
|
|
||||||
const [reserverName, setReserverName] = useState('');
|
const [reserverName, setReserverName] = useState('');
|
||||||
const [reserverContact, setReserverContact] = useState('');
|
const [reserverContact, setReserverContact] = useState('');
|
||||||
const [participantCount, setParticipantCount] = useState(2);
|
const [participantCount, setParticipantCount] = useState(theme.minParticipants || 1);
|
||||||
const [requirement, setRequirement] = useState('');
|
const [requirement, setRequirement] = useState('');
|
||||||
|
const [isLoadingUserInfo, setIsLoadingUserInfo] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUserInfo = async () => {
|
||||||
|
try {
|
||||||
|
const userContact = await fetchContact();
|
||||||
|
setReserverName(userContact.name || '');
|
||||||
|
setReserverContact(userContact.phone || '');
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('사용자 정보를 가져오지 못했습니다:', err);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingUserInfo(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUserInfo();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleError = (err: any) => {
|
const handleError = (err: any) => {
|
||||||
if (isLoginRequiredError(err)) {
|
if (isLoginRequiredError(err)) {
|
||||||
@ -26,10 +44,6 @@ const ReservationFormPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCountChange = (delta: number) => {
|
|
||||||
setParticipantCount(prev => Math.max(theme.minParticipants, Math.min(theme.maxParticipants, prev + delta)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePayment = () => {
|
const handlePayment = () => {
|
||||||
if (!reserverName || !reserverContact) {
|
if (!reserverName || !reserverContact) {
|
||||||
alert('예약자명과 연락처를 입력해주세요.');
|
alert('예약자명과 연락처를 입력해주세요.');
|
||||||
@ -46,7 +60,7 @@ const ReservationFormPage: React.FC = () => {
|
|||||||
|
|
||||||
createPendingReservation(reservationData)
|
createPendingReservation(reservationData)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
navigate('/v2-1/reservation/payment', {
|
navigate('/reservation/payment', {
|
||||||
state: {
|
state: {
|
||||||
reservationId: res.id,
|
reservationId: res.id,
|
||||||
themeName: theme.name,
|
themeName: theme.name,
|
||||||
@ -60,7 +74,6 @@ const ReservationFormPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!scheduleId || !theme) {
|
if (!scheduleId || !theme) {
|
||||||
// Handle case where state is not passed correctly
|
|
||||||
return (
|
return (
|
||||||
<div className="reservation-v21-container">
|
<div className="reservation-v21-container">
|
||||||
<h2 className="page-title">잘못된 접근</h2>
|
<h2 className="page-title">잘못된 접근</h2>
|
||||||
@ -85,11 +98,25 @@ const ReservationFormPage: React.FC = () => {
|
|||||||
<h3>예약자 정보</h3>
|
<h3>예약자 정보</h3>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="reserverName">예약자명</label>
|
<label htmlFor="reserverName">예약자명</label>
|
||||||
<input type="text" id="reserverName" value={reserverName} onChange={e => setReserverName(e.target.value)} />
|
<input
|
||||||
|
type="text"
|
||||||
|
id="reserverName"
|
||||||
|
value={reserverName}
|
||||||
|
onChange={e => setReserverName(e.target.value)}
|
||||||
|
disabled={isLoadingUserInfo}
|
||||||
|
placeholder={isLoadingUserInfo ? "로딩 중..." : "예약자명을 입력하세요"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="reserverContact">연락처</label>
|
<label htmlFor="reserverContact">연락처</label>
|
||||||
<input type="tel" id="reserverContact" value={reserverContact} onChange={e => setReserverContact(e.target.value)} placeholder="'-' 없이 입력"/>
|
<input
|
||||||
|
type="tel"
|
||||||
|
id="reserverContact"
|
||||||
|
value={reserverContact}
|
||||||
|
onChange={e => setReserverContact(e.target.value)}
|
||||||
|
disabled={isLoadingUserInfo}
|
||||||
|
placeholder={isLoadingUserInfo ? "로딩 중..." : "'-' 없이 입력"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>인원</label>
|
<label>인원</label>
|
||||||
@ -1,198 +0,0 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
import Flatpickr from 'react-flatpickr';
|
|
||||||
import 'flatpickr/dist/flatpickr.min.css';
|
|
||||||
import { fetchThemes } from '@_api/theme/themeAPI';
|
|
||||||
import { fetchTimesWithAvailability } from '@_api/time/timeAPI';
|
|
||||||
import { createReservationWithPayment, createWaiting } from '@_api/reservation/reservationAPI';
|
|
||||||
import type { ThemeRetrieveResponse } from '@_api/theme/themeTypes';
|
|
||||||
import type { TimeWithAvailabilityResponse } from '@_api/time/timeTypes';
|
|
||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
PaymentWidget: any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReservationPage: React.FC = () => {
|
|
||||||
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
|
|
||||||
const [themes, setThemes] = useState<ThemeRetrieveResponse[]>([]);
|
|
||||||
const [selectedTheme, setSelectedTheme] = useState<string | null>(null);
|
|
||||||
const [times, setTimes] = useState<TimeWithAvailabilityResponse[]>([]);
|
|
||||||
const [selectedTime, setSelectedTime] = useState<{ id: string, isAvailable: boolean } | null>(null);
|
|
||||||
const paymentWidgetRef = useRef<any>(null);
|
|
||||||
const paymentMethodsRef = useRef<any>(null);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const handleError = (err: any) => {
|
|
||||||
if (isLoginRequiredError(err)) {
|
|
||||||
alert('로그인이 필요해요.');
|
|
||||||
navigate('/login', { state: { from: location } });
|
|
||||||
} else {
|
|
||||||
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
|
|
||||||
alert(message);
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = 'https://js.tosspayments.com/v1/payment-widget';
|
|
||||||
script.async = true;
|
|
||||||
document.head.appendChild(script);
|
|
||||||
|
|
||||||
script.onload = () => {
|
|
||||||
const widgetClientKey = "test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm";
|
|
||||||
const paymentWidget = window.PaymentWidget(widgetClientKey, window.PaymentWidget.ANONYMOUS);
|
|
||||||
paymentWidgetRef.current = paymentWidget;
|
|
||||||
|
|
||||||
const paymentMethods = paymentWidget.renderPaymentMethods(
|
|
||||||
"#payment-method",
|
|
||||||
{ value: 1000 },
|
|
||||||
{ variantKey: "DEFAULT" }
|
|
||||||
);
|
|
||||||
paymentMethodsRef.current = paymentMethods;
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchThemes().then(res => setThemes(res.themes)).catch(handleError);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedDate && selectedTheme) {
|
|
||||||
const dateStr = selectedDate.toLocaleDateString('en-CA');
|
|
||||||
fetchTimesWithAvailability(dateStr, selectedTheme)
|
|
||||||
.then(res => {
|
|
||||||
setTimes(res.times);
|
|
||||||
setSelectedTime(null);
|
|
||||||
})
|
|
||||||
.catch(handleError);
|
|
||||||
}
|
|
||||||
}, [selectedDate, selectedTheme]);
|
|
||||||
|
|
||||||
const handleReservation = () => {
|
|
||||||
if (!selectedDate || !selectedTheme || !selectedTime || !paymentWidgetRef.current) {
|
|
||||||
alert('날짜, 테마, 시간을 모두 선택해주세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reservationData = {
|
|
||||||
date: selectedDate.toLocaleDateString('en-CA'),
|
|
||||||
themeId: selectedTheme,
|
|
||||||
timeId: selectedTime.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateRandomString = () =>
|
|
||||||
crypto.randomUUID().replace(/-/g, '');
|
|
||||||
|
|
||||||
paymentWidgetRef.current.requestPayment({
|
|
||||||
orderId: generateRandomString(),
|
|
||||||
orderName: "테스트 방탈출 예약 결제 1건",
|
|
||||||
amount: 1000,
|
|
||||||
}).then(function (data: any) {
|
|
||||||
const reservationPaymentRequest = {
|
|
||||||
...reservationData,
|
|
||||||
paymentKey: data.paymentKey,
|
|
||||||
orderId: data.orderId,
|
|
||||||
amount: data.amount,
|
|
||||||
paymentType: data.paymentType,
|
|
||||||
};
|
|
||||||
createReservationWithPayment(reservationPaymentRequest)
|
|
||||||
.then(() => {
|
|
||||||
alert("예약이 완료되었습니다.");
|
|
||||||
window.location.href = "/";
|
|
||||||
})
|
|
||||||
.catch(handleError);
|
|
||||||
}).catch(function (error: any) {
|
|
||||||
// This is a client-side error from Toss Payments, not our API
|
|
||||||
console.error("Payment request error:", error);
|
|
||||||
alert("결제 요청 중 오류가 발생했습니다.");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWaiting = () => {
|
|
||||||
if (!selectedDate || !selectedTheme || !selectedTime) {
|
|
||||||
alert('날짜, 테마, 시간을 모두 선택해주세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reservationData = {
|
|
||||||
date: selectedDate.toLocaleDateString('en-CA'),
|
|
||||||
themeId: selectedTheme,
|
|
||||||
timeId: selectedTime.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
createWaiting(reservationData)
|
|
||||||
.then(() => {
|
|
||||||
alert('예약 대기가 완료되었습니다.');
|
|
||||||
window.location.href = "/";
|
|
||||||
})
|
|
||||||
.catch(handleError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isReserveButtonDisabled = !selectedDate || !selectedTheme || !selectedTime || !selectedTime.isAvailable;
|
|
||||||
const isWaitButtonDisabled = !selectedDate || !selectedTheme || !selectedTime || selectedTime.isAvailable;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="content-container col-md-10 offset-md-1 p-5">
|
|
||||||
<h2 className="content-container-title">예약 페이지</h2>
|
|
||||||
<div className="d-flex" id="reservation-container">
|
|
||||||
<div className="section border rounded col-md-4 p-3" id="date-section">
|
|
||||||
<h3 className="fs-5 text-center mb-3">날짜 선택</h3>
|
|
||||||
<div className="d-flex justify-content-center p-3">
|
|
||||||
<Flatpickr
|
|
||||||
value={selectedDate || undefined}
|
|
||||||
onChange={([date]) => setSelectedDate(date)}
|
|
||||||
options={{ inline: true, defaultDate: new Date() }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`section border rounded col-md-4 p-3 ${!selectedDate ? 'disabled' : ''}`} id="theme-section">
|
|
||||||
<h3 className="fs-5 text-center mb-3">테마 선택</h3>
|
|
||||||
<div className="p-3" id="theme-slots">
|
|
||||||
{themes.map(theme => (
|
|
||||||
<div key={theme.id}
|
|
||||||
className={`theme-slot cursor-pointer bg-light border rounded p-3 mb-2 ${selectedTheme === theme.id ? 'active' : ''}`}
|
|
||||||
onClick={() => setSelectedTheme(theme.id)}>
|
|
||||||
{theme.name}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`section border rounded col-md-4 p-3 ${!selectedTheme ? 'disabled' : ''}`} id="time-section">
|
|
||||||
<h3 className="fs-5 text-center mb-3">시간 선택</h3>
|
|
||||||
<div className="p-3" id="time-slots">
|
|
||||||
{times.length > 0 ? times.map(time => (
|
|
||||||
<div key={time.id}
|
|
||||||
className={`time-slot cursor-pointer bg-light border rounded p-3 mb-2 ${selectedTime?.id === time.id ? 'active' : ''}`}
|
|
||||||
onClick={() => setSelectedTime({ id: time.id, isAvailable: time.isAvailable })}>
|
|
||||||
{time.startAt}
|
|
||||||
</div>
|
|
||||||
)) : <div className="no-times">선택할 수 있는 시간이 없습니다.</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="button-group float-end">
|
|
||||||
<button id="wait-button" className="btn btn-secondary mt-3" disabled={isWaitButtonDisabled} onClick={handleWaiting}>예약대기</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="wrapper w-100">
|
|
||||||
<div className="max-w-540 w-100">
|
|
||||||
<div id="payment-method" className="w-100"></div>
|
|
||||||
<div id="agreement" className="w-100"></div>
|
|
||||||
<div className="btn-wrapper w-100">
|
|
||||||
<button id="reserve-button" className="btn primary w-100" disabled={isReserveButtonDisabled} onClick={handleReservation}>예약하기</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReservationPage;
|
|
||||||
@ -1,49 +1,20 @@
|
|||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
import { isLoginRequiredError } from '@_api/apiClient';
|
||||||
import { holdSchedule, findAvailableThemesByDate, findSchedules } from '@_api/schedule/scheduleAPI';
|
import { findAvailableThemesByDate, findSchedules, holdSchedule } from '@_api/schedule/scheduleAPI';
|
||||||
import { ScheduleStatus, type ScheduleRetrieveResponse } from '@_api/schedule/scheduleTypes';
|
import { ScheduleStatus, type ScheduleRetrieveResponse } from '@_api/schedule/scheduleTypes';
|
||||||
import { findThemesByIds } from '@_api/theme/themeAPI';
|
import { findThemesByIds } from '@_api/theme/themeAPI';
|
||||||
import { Difficulty } from '@_api/theme/themeTypes';
|
import { mapThemeResponse, type UserThemeRetrieveResponse } from '@_api/theme/themeTypes';
|
||||||
import '@_css/reservation-v2-1.css';
|
import '@_css/reservation-v2-1.css';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
|
import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
|
||||||
|
|
||||||
interface ThemeV21 {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
difficulty: Difficulty;
|
|
||||||
description: string;
|
|
||||||
thumbnailUrl: string;
|
|
||||||
price: number;
|
|
||||||
minParticipants: number;
|
|
||||||
maxParticipants: number;
|
|
||||||
expectedMinutesFrom: number;
|
|
||||||
expectedMinutesTo: number;
|
|
||||||
availableMinutes: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDifficultyText = (difficulty: Difficulty): string => {
|
|
||||||
switch (difficulty) {
|
|
||||||
case Difficulty.VERY_EASY:
|
|
||||||
return '매우 쉬움';
|
|
||||||
case Difficulty.EASY:
|
|
||||||
return '쉬움';
|
|
||||||
case Difficulty.NORMAL:
|
|
||||||
return '보통';
|
|
||||||
case Difficulty.HARD:
|
|
||||||
return '어려움';
|
|
||||||
case Difficulty.VERY_HARD:
|
|
||||||
return '매우 어려움';
|
|
||||||
default:
|
|
||||||
return difficulty;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ReservationStep1PageV21: React.FC = () => {
|
const ReservationStep1Page: React.FC = () => {
|
||||||
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
||||||
const [viewDate, setViewDate] = useState<Date>(new Date()); // For carousel
|
const [viewDate, setViewDate] = useState<Date>(new Date()); // For carousel
|
||||||
const [themes, setThemes] = useState<ThemeV21[]>([]);
|
const [themes, setThemes] = useState<UserThemeRetrieveResponse[]>([]);
|
||||||
const [selectedTheme, setSelectedTheme] = useState<ThemeV21 | null>(null);
|
const [selectedTheme, setSelectedTheme] = useState<UserThemeRetrieveResponse | null>(null);
|
||||||
const [schedules, setSchedules] = useState<ScheduleRetrieveResponse[]>([]);
|
const [schedules, setSchedules] = useState<ScheduleRetrieveResponse[]>([]);
|
||||||
const [selectedSchedule, setSelectedSchedule] = useState<ScheduleRetrieveResponse | null>(null);
|
const [selectedSchedule, setSelectedSchedule] = useState<ScheduleRetrieveResponse | null>(null);
|
||||||
const [isThemeModalOpen, setIsThemeModalOpen] = useState(false);
|
const [isThemeModalOpen, setIsThemeModalOpen] = useState(false);
|
||||||
@ -77,9 +48,17 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(themeResponse => {
|
.then(themeResponse => {
|
||||||
setThemes(themeResponse.themes as ThemeV21[]);
|
setThemes(themeResponse.themes.map(mapThemeResponse));
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (isLoginRequiredError(err)) {
|
||||||
|
setThemes([]);
|
||||||
|
} else {
|
||||||
|
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
|
||||||
|
alert(message);
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(handleError)
|
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setSelectedTheme(null);
|
setSelectedTheme(null);
|
||||||
setSchedules([]);
|
setSchedules([]);
|
||||||
@ -96,7 +75,16 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
setSchedules(res.schedules);
|
setSchedules(res.schedules);
|
||||||
setSelectedSchedule(null);
|
setSelectedSchedule(null);
|
||||||
})
|
})
|
||||||
.catch(handleError);
|
.catch((err) => {
|
||||||
|
if (isLoginRequiredError(err)) {
|
||||||
|
setSchedules([]);
|
||||||
|
} else {
|
||||||
|
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
|
||||||
|
alert(message);
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
setSelectedSchedule(null);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [selectedDate, selectedTheme]);
|
}, [selectedDate, selectedTheme]);
|
||||||
|
|
||||||
@ -117,7 +105,7 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
|
|
||||||
holdSchedule(selectedSchedule.id)
|
holdSchedule(selectedSchedule.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
navigate('/v2/reservation/form', {
|
navigate('/reservation/form', {
|
||||||
state: {
|
state: {
|
||||||
scheduleId: selectedSchedule.id,
|
scheduleId: selectedSchedule.id,
|
||||||
theme: selectedTheme,
|
theme: selectedTheme,
|
||||||
@ -197,7 +185,7 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openThemeModal = (theme: ThemeV21) => {
|
const openThemeModal = (theme: UserThemeRetrieveResponse) => {
|
||||||
setSelectedTheme(theme);
|
setSelectedTheme(theme);
|
||||||
setIsThemeModalOpen(true);
|
setIsThemeModalOpen(true);
|
||||||
};
|
};
|
||||||
@ -237,7 +225,7 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
<h4>{theme.name}</h4>
|
<h4>{theme.name}</h4>
|
||||||
<div className="theme-meta">
|
<div className="theme-meta">
|
||||||
<p><strong>1인당 요금:</strong> {theme.price.toLocaleString()}원</p>
|
<p><strong>1인당 요금:</strong> {theme.price.toLocaleString()}원</p>
|
||||||
<p><strong>난이도:</strong> {getDifficultyText(theme.difficulty)}</p>
|
<p><strong>난이도:</strong> {theme.difficulty}</p>
|
||||||
<p><strong>참여 가능 인원:</strong> {theme.minParticipants} ~ {theme.maxParticipants}명</p>
|
<p><strong>참여 가능 인원:</strong> {theme.minParticipants} ~ {theme.maxParticipants}명</p>
|
||||||
<p><strong>예상 소요 시간:</strong> {theme.expectedMinutesFrom} ~ {theme.expectedMinutesTo}분</p>
|
<p><strong>예상 소요 시간:</strong> {theme.expectedMinutesFrom} ~ {theme.expectedMinutesTo}분</p>
|
||||||
<p><strong>이용 가능 시간:</strong> {theme.availableMinutes}분</p>
|
<p><strong>이용 가능 시간:</strong> {theme.availableMinutes}분</p>
|
||||||
@ -279,7 +267,7 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
<h2>{selectedTheme.name}</h2>
|
<h2>{selectedTheme.name}</h2>
|
||||||
<div className="modal-section">
|
<div className="modal-section">
|
||||||
<h3>테마 정보</h3>
|
<h3>테마 정보</h3>
|
||||||
<p><strong>난이도:</strong> {getDifficultyText(selectedTheme.difficulty)}</p>
|
<p><strong>난이도:</strong> {selectedTheme.difficulty}</p>
|
||||||
<p><strong>참여 인원:</strong> {selectedTheme.minParticipants} ~ {selectedTheme.maxParticipants}명</p>
|
<p><strong>참여 인원:</strong> {selectedTheme.minParticipants} ~ {selectedTheme.maxParticipants}명</p>
|
||||||
<p><strong>소요 시간:</strong> {selectedTheme.expectedMinutesFrom} ~ {selectedTheme.expectedMinutesTo}분</p>
|
<p><strong>소요 시간:</strong> {selectedTheme.expectedMinutesFrom} ~ {selectedTheme.expectedMinutesTo}분</p>
|
||||||
<p><strong>1인당 요금:</strong> {selectedTheme.price.toLocaleString()}원</p>
|
<p><strong>1인당 요금:</strong> {selectedTheme.price.toLocaleString()}원</p>
|
||||||
@ -313,4 +301,4 @@ const ReservationStep1PageV21: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ReservationStep1PageV21;
|
export default ReservationStep1Page;
|
||||||
@ -13,7 +13,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReservationStep2PageV21: React.FC = () => {
|
const ReservationStep2Page: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const paymentWidgetRef = useRef<any>(null);
|
const paymentWidgetRef = useRef<any>(null);
|
||||||
@ -35,7 +35,7 @@ const ReservationStep2PageV21: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!reservationId) {
|
if (!reservationId) {
|
||||||
alert('잘못된 접근입니다.');
|
alert('잘못된 접근입니다.');
|
||||||
navigate('/v2-1/reservation');
|
navigate('/reservation');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ const ReservationStep2PageV21: React.FC = () => {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
alert('결제가 완료되었어요!');
|
alert('결제가 완료되었어요!');
|
||||||
navigate('/v2-1/reservation/success', {
|
navigate('/reservation/success', {
|
||||||
state: {
|
state: {
|
||||||
themeName,
|
themeName,
|
||||||
date,
|
date,
|
||||||
@ -128,4 +128,4 @@ const ReservationStep2PageV21: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ReservationStep2PageV21;
|
export default ReservationStep2Page;
|
||||||
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
|
import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
|
||||||
|
|
||||||
const ReservationSuccessPageV21: React.FC = () => {
|
const ReservationSuccessPage: React.FC = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { themeName, date, startAt } = (location.state as {
|
const { themeName, date, startAt } = (location.state as {
|
||||||
themeName: string;
|
themeName: string;
|
||||||
@ -25,7 +25,7 @@ const ReservationSuccessPageV21: React.FC = () => {
|
|||||||
<p><strong>시간:</strong> {formattedTime}</p>
|
<p><strong>시간:</strong> {formattedTime}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="success-page-actions">
|
<div className="success-page-actions">
|
||||||
<Link to="/my-reservation/v2" className="action-button">
|
<Link to="/my-reservation" className="action-button">
|
||||||
내 예약 목록
|
내 예약 목록
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/" className="action-button secondary">
|
<Link to="/" className="action-button secondary">
|
||||||
@ -36,4 +36,4 @@ const ReservationSuccessPageV21: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ReservationSuccessPageV21;
|
export default ReservationSuccessPage;
|
||||||
@ -1,204 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
cancelReservationByAdmin,
|
|
||||||
createReservationByAdmin,
|
|
||||||
fetchReservations,
|
|
||||||
searchReservations
|
|
||||||
} from '@_api/reservation/reservationAPI';
|
|
||||||
import { fetchMembers } from '@_api/member/memberAPI';
|
|
||||||
import { fetchThemes } from '@_api/theme/themeAPI';
|
|
||||||
import { fetchTimes } from '@_api/time/timeAPI';
|
|
||||||
import type { ReservationRetrieveResponse } from '@_api/reservation/reservationTypes';
|
|
||||||
import type { MemberRetrieveResponse } from '@_api/member/memberTypes';
|
|
||||||
import type { ThemeRetrieveResponse } from '@_api/theme/themeTypes';
|
|
||||||
import type { TimeRetrieveResponse } from '@_api/time/timeTypes';
|
|
||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
|
||||||
import '../../css/admin-reservation-page.css';
|
|
||||||
|
|
||||||
const AdminReservationPage: React.FC = () => {
|
|
||||||
const [reservations, setReservations] = useState<ReservationRetrieveResponse[]>([]);
|
|
||||||
const [members, setMembers] = useState<MemberRetrieveResponse[]>([]);
|
|
||||||
const [themes, setThemes] = useState<ThemeRetrieveResponse[]>([]);
|
|
||||||
const [times, setTimes] = useState<TimeRetrieveResponse[]>([]);
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
const [newReservation, setNewReservation] = useState({ memberId: '', themeId: '', date: '', timeId: '' });
|
|
||||||
const [filter, setFilter] = useState({ memberId: '', themeId: '', dateFrom: '', dateTo: '' });
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const handleError = (err: any) => {
|
|
||||||
if (isLoginRequiredError(err)) {
|
|
||||||
alert('로그인이 필요해요.');
|
|
||||||
navigate('/login', { state: { from: location } });
|
|
||||||
} else {
|
|
||||||
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
|
|
||||||
alert(message);
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
_fetchReservations();
|
|
||||||
fetchMembers().then(res => setMembers(res.members)).catch(handleError);
|
|
||||||
fetchThemes().then(res => setThemes(res.themes)).catch(handleError);
|
|
||||||
fetchTimes().then(res => setTimes(res.times)).catch(handleError);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const _fetchReservations = () => {
|
|
||||||
fetchReservations()
|
|
||||||
.then(res => setReservations(res.reservations))
|
|
||||||
.catch(handleError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
|
||||||
setFilter({ ...filter, [e.target.name]: e.target.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyFilter = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const params = {
|
|
||||||
memberId: filter.memberId ? filter.memberId : undefined,
|
|
||||||
themeId: filter.themeId ? filter.themeId : undefined,
|
|
||||||
dateFrom: filter.dateFrom,
|
|
||||||
dateTo: filter.dateTo,
|
|
||||||
};
|
|
||||||
searchReservations(params)
|
|
||||||
.then(res => setReservations(res.reservations))
|
|
||||||
.catch(handleError);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddClick = () => setIsEditing(true);
|
|
||||||
const handleCancelClick = () => setIsEditing(false);
|
|
||||||
|
|
||||||
const handleSaveClick = async () => {
|
|
||||||
if (!newReservation.memberId || !newReservation.themeId || !newReservation.date || !newReservation.timeId) {
|
|
||||||
alert('모든 필드를 입력해주세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const request = {
|
|
||||||
memberId: newReservation.memberId,
|
|
||||||
themeId: newReservation.themeId,
|
|
||||||
date: newReservation.date,
|
|
||||||
timeId: newReservation.timeId,
|
|
||||||
};
|
|
||||||
await createReservationByAdmin(request)
|
|
||||||
.then(() => {
|
|
||||||
alert('예약을 추가했어요. 결제는 별도로 진행해주세요.');
|
|
||||||
_fetchReservations();
|
|
||||||
handleCancelClick();
|
|
||||||
})
|
|
||||||
.catch(handleError);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteReservation = async(id: string) => {
|
|
||||||
if (!window.confirm('정말 삭제하시겠어요?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await cancelReservationByAdmin(id)
|
|
||||||
.then(() => {
|
|
||||||
setReservations(reservations.filter(r => r.id !== id))
|
|
||||||
alert('예약을 삭제했어요.');
|
|
||||||
}).catch(handleError);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="admin-reservation-container">
|
|
||||||
<h2 className="page-title">예약 관리</h2>
|
|
||||||
<div className="admin-reservation-content">
|
|
||||||
<div className="reservations-main section-card">
|
|
||||||
<div className="table-header">
|
|
||||||
<button className="btn btn-primary" onClick={handleAddClick}>예약 추가</button>
|
|
||||||
</div>
|
|
||||||
<div className="table-container">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>예약번호</th>
|
|
||||||
<th>예약자</th>
|
|
||||||
<th>테마</th>
|
|
||||||
<th>날짜</th>
|
|
||||||
<th>시간</th>
|
|
||||||
<th>상태</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{reservations.map(r => (
|
|
||||||
<tr key={r.id}>
|
|
||||||
<td>{r.id}</td>
|
|
||||||
<td>{r.member.name}</td>
|
|
||||||
<td>{r.theme.name}</td>
|
|
||||||
<td>{r.date}</td>
|
|
||||||
<td>{r.time.startAt}</td>
|
|
||||||
<td>{r.status === 'CONFIRMED' ? '결제 완료' : '결제 대기'}</td>
|
|
||||||
<td><button className="btn btn-danger" onClick={() => deleteReservation(r.id)}>삭제</button></td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{isEditing && (
|
|
||||||
<tr className="editing-row">
|
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
<select className="form-select" onChange={e => setNewReservation({ ...newReservation, memberId: e.target.value })}>
|
|
||||||
<option value="">멤버 선택</option>
|
|
||||||
{members.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<select className="form-select" onChange={e => setNewReservation({ ...newReservation, themeId: e.target.value })}>
|
|
||||||
<option value="">테마 선택</option>
|
|
||||||
{themes.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td><input type="date" className="form-input" onChange={e => setNewReservation({ ...newReservation, date: e.target.value })} /></td>
|
|
||||||
<td>
|
|
||||||
<select className="form-select" onChange={e => setNewReservation({ ...newReservation, timeId: e.target.value })}>
|
|
||||||
<option value="">시간 선택</option>
|
|
||||||
{times.map(t => <option key={t.id} value={t.id}>{t.startAt}</option>)}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
<button className="btn btn-primary" onClick={handleSaveClick}>확인</button>
|
|
||||||
<button className="btn btn-secondary" onClick={handleCancelClick}>취소</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="filter-section section-card">
|
|
||||||
<h3 className="card-title">검색 필터</h3>
|
|
||||||
<form id="filter-form" onSubmit={applyFilter}>
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label" htmlFor="member">예약자</label>
|
|
||||||
<select id="member" name="memberId" className="form-select" onChange={handleFilterChange}>
|
|
||||||
<option value="">전체</option>
|
|
||||||
{members.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label" htmlFor="theme">테마</label>
|
|
||||||
<select id="theme" name="themeId" className="form-select" onChange={handleFilterChange}>
|
|
||||||
<option value="">전체</option>
|
|
||||||
{themes.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label" htmlFor="date-from">From</label>
|
|
||||||
<input type="date" id="date-from" name="dateFrom" className="form-input" onChange={handleFilterChange} />
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label" htmlFor="date-to">To</label>
|
|
||||||
<input type="date" id="date-to" name="dateTo" className="form-input" onChange={handleFilterChange} />
|
|
||||||
</div>
|
|
||||||
<button type="submit" className="btn btn-primary">적용</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdminReservationPage;
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
import { fetchAdminThemes } from '@_api/theme/themeAPI';
|
|
||||||
import type {AdminThemeSummaryRetrieveResponse} from '@_api/theme/themeTypes';
|
|
||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
|
||||||
import '../../css/admin-theme-page.css';
|
|
||||||
|
|
||||||
const AdminThemePage: React.FC = () => {
|
|
||||||
const [themes, setThemes] = useState<AdminThemeSummaryRetrieveResponse[]>([]);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const handleError = (err: any) => {
|
|
||||||
if (isLoginRequiredError(err)) {
|
|
||||||
alert('로그인이 필요해요.');
|
|
||||||
navigate('/login', { state: { from: location } });
|
|
||||||
} else {
|
|
||||||
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
|
|
||||||
alert(message);
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetchAdminThemes();
|
|
||||||
setThemes(response.themes);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleAddClick = () => {
|
|
||||||
navigate('/admin/theme/edit/new');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleManageClick = (themeId: string) => {
|
|
||||||
navigate(`/admin/theme/edit/${themeId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="admin-theme-container">
|
|
||||||
<h2 className="page-title">테마 관리</h2>
|
|
||||||
<div className="section-card">
|
|
||||||
<div className="table-header">
|
|
||||||
<button className="btn btn-primary" onClick={handleAddClick}>테마 추가</button>
|
|
||||||
</div>
|
|
||||||
<div className="table-container">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>이름</th>
|
|
||||||
<th>난이도</th>
|
|
||||||
<th>1인당 요금</th>
|
|
||||||
<th>공개여부</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{themes.map(theme => (
|
|
||||||
<tr key={theme.id}>
|
|
||||||
<td>{theme.name}</td>
|
|
||||||
<td>{theme.difficulty}</td>
|
|
||||||
<td>{theme.price.toLocaleString()}원</td>
|
|
||||||
<td>{theme.isOpen ? '공개' : '비공개'}</td>
|
|
||||||
<td>
|
|
||||||
<button className="btn btn-secondary" onClick={() => handleManageClick(theme.id)}>관리</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdminThemePage;
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
import { createTime, delTime, fetchTimes } from '@_api/time/timeAPI';
|
|
||||||
import type { TimeCreateRequest } from '@_api/time/timeTypes';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
|
||||||
import '../../css/admin-time-page.css';
|
|
||||||
|
|
||||||
const AdminTimePage: React.FC = () => {
|
|
||||||
const [times, setTimes] = useState<any[]>([]);
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
const [newTime, setNewTime] = useState('');
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const handleError = (err: any) => {
|
|
||||||
if (isLoginRequiredError(err)) {
|
|
||||||
alert('로그인이 필요해요.');
|
|
||||||
navigate('/login', { state: { from: location } });
|
|
||||||
} else {
|
|
||||||
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
|
|
||||||
alert(message);
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
await fetchTimes()
|
|
||||||
.then(response => setTimes(response.times))
|
|
||||||
.catch(handleError);
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleAddClick = () => {
|
|
||||||
setIsEditing(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancelClick = () => {
|
|
||||||
setIsEditing(false);
|
|
||||||
setNewTime('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveClick = async () => {
|
|
||||||
if (!newTime) {
|
|
||||||
alert('시간을 입력해주세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!/^\d{2}:\d{2}$/.test(newTime)) {
|
|
||||||
alert('시간 형식이 올바르지 않습니다. HH:MM 형식으로 입력해주세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const request: TimeCreateRequest = {
|
|
||||||
startAt: newTime
|
|
||||||
};
|
|
||||||
|
|
||||||
await createTime(request)
|
|
||||||
.then((response) => {
|
|
||||||
setTimes([...times, response]);
|
|
||||||
alert('시간을 추가했어요.');
|
|
||||||
handleCancelClick();
|
|
||||||
})
|
|
||||||
.catch(handleError);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteTime = async (id: string) => {
|
|
||||||
if (!window.confirm('정말 삭제하시겠어요?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await delTime(id)
|
|
||||||
.then(() => {
|
|
||||||
setTimes(times.filter(time => time.id !== id));
|
|
||||||
alert('시간을 삭제했어요.');
|
|
||||||
})
|
|
||||||
.catch(handleError);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="admin-time-container">
|
|
||||||
<h2 className="page-title">시간 관리</h2>
|
|
||||||
<div className="section-card">
|
|
||||||
<div className="table-header">
|
|
||||||
<button className="btn btn-primary" onClick={handleAddClick}>시간 추가</button>
|
|
||||||
</div>
|
|
||||||
<div className="table-container">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>시간</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{times.map(time => (
|
|
||||||
<tr key={time.id}>
|
|
||||||
<td>{time.id}</td>
|
|
||||||
<td>{time.startAt}</td>
|
|
||||||
<td>
|
|
||||||
<button className="btn btn-danger" onClick={() => deleteTime(time.id)}>삭제</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{isEditing && (
|
|
||||||
<tr className="editing-row">
|
|
||||||
<td></td>
|
|
||||||
<td><input type="time" className="form-input" value={newTime} onChange={e => setNewTime(e.target.value)} /></td>
|
|
||||||
<td>
|
|
||||||
<button className="btn btn-primary" onClick={handleSaveClick}>확인</button>
|
|
||||||
<button className="btn btn-secondary" onClick={handleCancelClick}>취소</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdminTimePage;
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
import { confirmWaiting, fetchWaitingReservations, rejectWaiting } from '@_api/reservation/reservationAPI';
|
|
||||||
import type { ReservationRetrieveResponse } from '@_api/reservation/reservationTypes';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
|
||||||
import '../../css/admin-waiting-page.css';
|
|
||||||
|
|
||||||
const AdminWaitingPage: React.FC = () => {
|
|
||||||
const [waitings, setWaitings] = useState<ReservationRetrieveResponse[]>([]);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const handleError = (err: any) => {
|
|
||||||
if (isLoginRequiredError(err)) {
|
|
||||||
alert('로그인이 필요해요.');
|
|
||||||
navigate('/login', { state: { from: location } });
|
|
||||||
} else {
|
|
||||||
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
|
|
||||||
alert(message);
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
await fetchWaitingReservations()
|
|
||||||
.then(res => setWaitings(res.reservations))
|
|
||||||
.catch(handleError);
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const approveWaiting = async (id: string) => {
|
|
||||||
await confirmWaiting(id)
|
|
||||||
.then(() => {
|
|
||||||
alert('대기 중인 예약을 승인했어요. 결제는 별도로 진행해주세요.');
|
|
||||||
setWaitings(waitings.filter(w => w.id !== id));
|
|
||||||
})
|
|
||||||
.catch(handleError);
|
|
||||||
};
|
|
||||||
|
|
||||||
const denyWaiting = async (id: string) => {
|
|
||||||
await rejectWaiting(id)
|
|
||||||
.then(() => {
|
|
||||||
alert('대기 중인 예약을 거절했어요.');
|
|
||||||
setWaitings(waitings.filter(w => w.id !== id));
|
|
||||||
})
|
|
||||||
.catch(handleError);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="admin-waiting-container">
|
|
||||||
<h2 className="page-title">예약 대기 관리</h2>
|
|
||||||
<div className="section-card">
|
|
||||||
<div className="table-container">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>예약대기 번호</th>
|
|
||||||
<th>예약자</th>
|
|
||||||
<th>테마</th>
|
|
||||||
<th>날짜</th>
|
|
||||||
<th>시간</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{waitings.map(w => (
|
|
||||||
<tr key={w.id}>
|
|
||||||
<td>{w.id}</td>
|
|
||||||
<td>{w.member.name}</td>
|
|
||||||
<td>{w.theme.name}</td>
|
|
||||||
<td>{w.date}</td>
|
|
||||||
<td>{w.time.startAt}</td>
|
|
||||||
<td>
|
|
||||||
<button className="btn btn-primary" onClick={() => approveWaiting(w.id)}>승인</button>
|
|
||||||
<button className="btn btn-danger" onClick={() => denyWaiting(w.id)}>거절</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdminWaitingPage;
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
import {fetchMostReservedThemeIds} from '@_api/reservation/reservationAPIV2';
|
|
||||||
import '@_css/home-page-v2.css';
|
|
||||||
import React, {useEffect, useState} from 'react';
|
|
||||||
import {useNavigate} from 'react-router-dom';
|
|
||||||
import {findThemesByIds} from '../../api/theme/themeAPI';
|
|
||||||
import {type UserThemeRetrieveResponse} from '../../api/theme/themeTypes';
|
|
||||||
|
|
||||||
const HomePageV2: React.FC = () => {
|
|
||||||
const [ranking, setRanking] = useState<UserThemeRetrieveResponse[]>([]);
|
|
||||||
const [selectedTheme, setSelectedTheme] = useState<UserThemeRetrieveResponse | null>(null);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const themeIds = await fetchMostReservedThemeIds().then(res => {
|
|
||||||
const themeIds = res.themeIds;
|
|
||||||
if (themeIds.length === 0) {
|
|
||||||
setRanking([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return themeIds;
|
|
||||||
})
|
|
||||||
|
|
||||||
if (themeIds === undefined) return;
|
|
||||||
if (themeIds.length === 0) return;
|
|
||||||
|
|
||||||
const response = await findThemesByIds({ themeIds: themeIds });
|
|
||||||
setRanking(response.themes);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching ranking:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleThemeClick = (theme: UserThemeRetrieveResponse) => {
|
|
||||||
setSelectedTheme(theme);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
|
||||||
setSelectedTheme(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReservationClick = (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (selectedTheme) {
|
|
||||||
navigate('/v2-1/reservation', { state: { themeId: selectedTheme.id } });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="home-container-v2">
|
|
||||||
<h2 className="page-title">인기 테마</h2>
|
|
||||||
<div className="theme-ranking-list-v2">
|
|
||||||
{ranking.map(theme => (
|
|
||||||
<div key={theme.id} className="theme-ranking-item-v2" onClick={() => handleThemeClick(theme)}>
|
|
||||||
<img className="thumbnail" src={theme.thumbnailUrl} alt={theme.name} />
|
|
||||||
<div className="theme-info">
|
|
||||||
<h5 className="theme-name">{theme.name}</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedTheme && (
|
|
||||||
<div className="theme-modal-overlay" onClick={handleCloseModal}>
|
|
||||||
<div className="theme-modal-content" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<img className="modal-thumbnail" src={selectedTheme.thumbnailUrl} alt={selectedTheme.name} />
|
|
||||||
<div className="modal-theme-info">
|
|
||||||
<h2>{selectedTheme.name}</h2>
|
|
||||||
<p>{selectedTheme.description}</p>
|
|
||||||
<div className="theme-details">
|
|
||||||
<p><strong>난이도:</strong> {selectedTheme.difficulty}</p>
|
|
||||||
<p><strong>가격:</strong> {selectedTheme.price.toLocaleString()}원</p>
|
|
||||||
<p><strong>예상 시간:</strong> {selectedTheme.expectedMinutesFrom} ~ {selectedTheme.expectedMinutesTo}분</p>
|
|
||||||
<p><strong>이용 가능 인원:</strong> {selectedTheme.minParticipants} ~ {selectedTheme.maxParticipants}명</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="modal-buttons">
|
|
||||||
<button onClick={handleReservationClick} className="modal-button reserve">예약하기</button>
|
|
||||||
<button onClick={handleCloseModal} className="modal-button close">닫기</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HomePageV2;
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
import { useAuth } from '../../context/AuthContext';
|
|
||||||
import '../../css/login-page-v2.css';
|
|
||||||
|
|
||||||
const LoginPageV2: React.FC = () => {
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const { login } = useAuth();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const from = location.state?.from?.pathname || '/';
|
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
await login({email, password});
|
|
||||||
|
|
||||||
alert('로그인에 성공했어요!');
|
|
||||||
navigate(from, { replace: true });
|
|
||||||
} catch (error: any) {
|
|
||||||
const message = error.response?.data?.message || '로그인에 실패했어요. 이메일과 비밀번호를 확인해주세요.';
|
|
||||||
alert(message);
|
|
||||||
console.error('로그인 실패:', error);
|
|
||||||
setPassword('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="login-container-v2">
|
|
||||||
<h2 className="page-title">로그인</h2>
|
|
||||||
<form className="login-form-v2" onSubmit={handleLogin}>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
className="form-input"
|
|
||||||
placeholder="이메일"
|
|
||||||
value={email}
|
|
||||||
onChange={e => setEmail(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
className="form-input"
|
|
||||||
placeholder="비밀번호"
|
|
||||||
value={password}
|
|
||||||
onChange={e => setPassword(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="button-group">
|
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => navigate('/signup')}>회원가입</button>
|
|
||||||
<button type="submit" className="btn btn-primary">로그인</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LoginPageV2;
|
|
||||||
@ -1,342 +0,0 @@
|
|||||||
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 '../../css/my-reservation-v2.css';
|
|
||||||
|
|
||||||
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;
|
|
||||||
onCancelSubmit: (reason: string) => void;
|
|
||||||
onBack: () => void;
|
|
||||||
isCancelling: boolean;
|
|
||||||
}> = ({ reservation, onCancelSubmit, onBack, isCancelling }) => {
|
|
||||||
const [reason, setReason] = useState('');
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
if (!reason.trim()) {
|
|
||||||
alert('취소 사유를 입력해주세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onCancelSubmit(reason);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="cancellation-view-v2">
|
|
||||||
<h3>취소 정보 확인</h3>
|
|
||||||
<div className="cancellation-summary-v2">
|
|
||||||
<p><strong>테마:</strong> {reservation.themeName}</p>
|
|
||||||
<p><strong>신청 일시:</strong> {formatDisplayDateTime(reservation.applicationDateTime)}</p>
|
|
||||||
<p><strong>결제 금액:</strong> {reservation.payment.totalAmount.toLocaleString()}원</p>
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
value={reason}
|
|
||||||
onChange={(e) => setReason(e.target.value)}
|
|
||||||
placeholder="취소 사유를 입력해주세요."
|
|
||||||
className="cancellation-reason-textarea-v2"
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
<div className="modal-actions-v2">
|
|
||||||
<button onClick={onBack} className="back-button-v2" disabled={isCancelling}>뒤로가기</button>
|
|
||||||
<button onClick={handleSubmit} className="cancel-submit-button-v2" disabled={isCancelling}>
|
|
||||||
{isCancelling ? '취소 중...' : '취소 요청'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const ReservationDetailView: React.FC<{
|
|
||||||
reservation: ReservationDetail;
|
|
||||||
onGoToCancel: () => void;
|
|
||||||
}> = ({ reservation, onGoToCancel }) => {
|
|
||||||
|
|
||||||
const renderPaymentDetails = (payment: PaymentRetrieveResponse) => {
|
|
||||||
const { detail } = payment;
|
|
||||||
|
|
||||||
switch (detail.type) {
|
|
||||||
case 'CARD':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p><strong>주문 ID:</strong> {payment.orderId}</p>
|
|
||||||
{payment.totalAmount === detail.amount ? (
|
|
||||||
<p><strong>결제 금액:</strong> {payment.totalAmount.toLocaleString()}원</p>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p><strong>전체 금액:</strong> {payment.totalAmount.toLocaleString()}원</p>
|
|
||||||
<p><strong>승인 금액:</strong> {detail.amount.toLocaleString()}원</p>
|
|
||||||
{detail.easypayDiscountAmount && (
|
|
||||||
<p><strong>할인 금액:</strong> {detail.easypayDiscountAmount.toLocaleString()}원</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<p><strong>결제 수단:</strong> {detail.easypayProviderName ? `간편결제 / ${detail.easypayProviderName}` : '카드'}</p>
|
|
||||||
<p><strong>카드사 / 구분:</strong> {detail.issuerCode}({detail.ownerType}) / {detail.cardType}</p>
|
|
||||||
<p><strong>카드 번호:</strong> {detail.cardNumber}</p>
|
|
||||||
<p><strong>할부 방식:</strong> {detail.installmentPlanMonths === 0 ? '일시불' : `${detail.installmentPlanMonths}개월`}</p>
|
|
||||||
<p><strong>승인 번호:</strong> {detail.approvalNumber}</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case 'BANK_TRANSFER':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p><strong>결제 수단:</strong> 계좌이체</p>
|
|
||||||
<p><strong>은행:</strong> {detail.bankName}</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case 'EASYPAY_PREPAID':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p><strong>결제 수단:</strong> 간편결제 / {detail.providerName}</p>
|
|
||||||
<p><strong>총 금액 :</strong> {payment.totalAmount.toLocaleString()}원</p>
|
|
||||||
<p><strong>결제 금액:</strong> {detail.amount.toLocaleString()}원</p>
|
|
||||||
{detail.discountAmount > 0 && <p><strong>포인트:</strong> {detail.discountAmount.toLocaleString()}원</p>}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <p><strong>결제 수단:</strong> {payment.method}</p>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="modal-section-v2">
|
|
||||||
<h3>예약 정보</h3>
|
|
||||||
<p><strong>예약 테마:</strong> {reservation.themeName}</p>
|
|
||||||
<p><strong>이용 예정일:</strong> {formatCardDateTime(reservation.date, reservation.startAt)}</p>
|
|
||||||
<p><strong>예약자 이름:</strong> {reservation.member.name}</p>
|
|
||||||
<p><strong>예약자 이메일:</strong> {reservation.member.email}</p>
|
|
||||||
<p><strong>예약 신청 일시:</strong> {formatDisplayDateTime(reservation.applicationDateTime)}</p>
|
|
||||||
</div>
|
|
||||||
<div className="modal-section-v2">
|
|
||||||
<h3>결제 정보</h3>
|
|
||||||
{/* <p><strong>결제금액:</strong> {reservation.payment.totalAmount.toLocaleString()}원</p> */}
|
|
||||||
{renderPaymentDetails(reservation.payment)}
|
|
||||||
<p><strong>결제 승인 일시:</strong> {formatDisplayDateTime(reservation.payment.approvedAt)}</p>
|
|
||||||
</div>
|
|
||||||
{reservation.payment.cancellation && (
|
|
||||||
<div className="modal-section-v2 cancellation-section-v2">
|
|
||||||
<h3>취소 정보</h3>
|
|
||||||
<p><strong>취소 요청 일시:</strong> {formatDisplayDateTime(reservation.payment.cancellation.cancellationRequestedAt)}</p>
|
|
||||||
<p><strong>환불 완료 일시:</strong> {formatDisplayDateTime(reservation.payment.cancellation.cancellationApprovedAt)}</p>
|
|
||||||
<p><strong>취소 사유:</strong> {reservation.payment.cancellation.cancelReason}</p>
|
|
||||||
<p><strong>취소 요청자:</strong> {reservation.payment.cancellation.canceledBy == reservation.member.id ? '회원 본인' : '관리자'}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{reservation.payment.status !== 'CANCELED' && (
|
|
||||||
<div className="modal-actions-v2">
|
|
||||||
<button onClick={onGoToCancel} className="cancel-button-v2">예약 취소하기</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Main Page Component ---
|
|
||||||
const MyReservationPageV2: React.FC = () => {
|
|
||||||
const [reservations, setReservations] = useState<ReservationSummaryRetrieveResponse[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const [selectedReservation, setSelectedReservation] = useState<ReservationDetail | null>(null);
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [isDetailLoading, setIsDetailLoading] = useState(false);
|
|
||||||
const [detailError, setDetailError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const [modalView, setModalView] = useState<'detail' | 'cancel'>('detail');
|
|
||||||
const [isCancelling, setIsCancelling] = useState(false);
|
|
||||||
|
|
||||||
const loadReservations = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const data = await fetchSummaryByMember();
|
|
||||||
setReservations(data.reservations);
|
|
||||||
setError(null);
|
|
||||||
} catch (err) {
|
|
||||||
setError('예약 목록을 불러오는 데 실패했습니다.');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadReservations();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleShowDetail = async (id: string, themeName: string, date: string, time: string) => {
|
|
||||||
try {
|
|
||||||
setIsDetailLoading(true);
|
|
||||||
setDetailError(null);
|
|
||||||
setModalView('detail');
|
|
||||||
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('예약 상세 정보를 불러오는 데 실패했습니다.');
|
|
||||||
} finally {
|
|
||||||
setIsDetailLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
|
||||||
setIsModalOpen(false);
|
|
||||||
setSelectedReservation(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancelSubmit = async (reason: string) => {
|
|
||||||
if (!selectedReservation) return;
|
|
||||||
|
|
||||||
if (!window.confirm('정말 취소하시겠어요?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsCancelling(true);
|
|
||||||
setDetailError(null);
|
|
||||||
await cancelReservation(selectedReservation.id, reason);
|
|
||||||
cancelPayment({ reservationId: selectedReservation.id, cancelReason: reason });
|
|
||||||
alert('예약을 취소했어요. 결제 취소까지는 3-5일 정도 소요될 수 있어요.');
|
|
||||||
handleCloseModal();
|
|
||||||
await loadReservations(); // Refresh the list
|
|
||||||
} catch (err) {
|
|
||||||
setDetailError(err instanceof Error ? err.message : '예약 취소에 실패했습니다.');
|
|
||||||
} finally {
|
|
||||||
setIsCancelling(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
console.log("reservations=", reservations);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="my-reservation-container-v2">
|
|
||||||
<h1>내 예약 V2</h1>
|
|
||||||
|
|
||||||
{isLoading && <p>목록 로딩 중...</p>}
|
|
||||||
{error && <p className="error-message-v2">{error}</p>}
|
|
||||||
|
|
||||||
{!isLoading && !error && (
|
|
||||||
<div className="reservation-list-v2">
|
|
||||||
{reservations.map((res) => (
|
|
||||||
<div key={res.id} className={`reservation-summary-card-v2 status-${res.status.toString().toLowerCase()}`}>
|
|
||||||
<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>
|
|
||||||
<button
|
|
||||||
onClick={() => handleShowDetail(res.id, res.themeName, res.date, res.startAt)}
|
|
||||||
disabled={isDetailLoading}
|
|
||||||
className="detail-button-v2"
|
|
||||||
>
|
|
||||||
{isDetailLoading && selectedReservation?.id === res.id ? '로딩중...' : '상세보기'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isModalOpen && selectedReservation && (
|
|
||||||
<div className="modal-overlay-v2" onClick={handleCloseModal}>
|
|
||||||
<div className="modal-content-v2" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<button className="modal-close-button-v2" onClick={handleCloseModal}>×</button>
|
|
||||||
<h2>{modalView === 'detail' ? '예약 상세 정보' : '예약 취소'}</h2>
|
|
||||||
{detailError && <p className="error-message-v2">{detailError}</p>}
|
|
||||||
|
|
||||||
{modalView === 'detail' ? (
|
|
||||||
<ReservationDetailView
|
|
||||||
reservation={selectedReservation}
|
|
||||||
onGoToCancel={() => setModalView('cancel')}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<CancellationView
|
|
||||||
reservation={selectedReservation}
|
|
||||||
onCancelSubmit={handleCancelSubmit}
|
|
||||||
onBack={() => setModalView('detail')}
|
|
||||||
isCancelling={isCancelling}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MyReservationPageV2;
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
import Flatpickr from 'react-flatpickr';
|
|
||||||
import 'flatpickr/dist/flatpickr.min.css';
|
|
||||||
import '@_css/reservation-v2.css';
|
|
||||||
import { fetchThemes } from '@_api/theme/themeAPI';
|
|
||||||
import { fetchTimesWithAvailability } from '@_api/time/timeAPI';
|
|
||||||
import { createPendingReservation } from '@_api/reservation/reservationAPI';
|
|
||||||
import type { ThemeRetrieveResponse } from '@_api/theme/themeTypes';
|
|
||||||
import type { TimeWithAvailabilityResponse } from '@_api/time/timeTypes';
|
|
||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
|
||||||
|
|
||||||
const ReservationStep1Page: React.FC = () => {
|
|
||||||
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
|
|
||||||
const [themes, setThemes] = useState<ThemeRetrieveResponse[]>([]);
|
|
||||||
const [selectedTheme, setSelectedTheme] = useState<ThemeRetrieveResponse | null>(null);
|
|
||||||
const [times, setTimes] = useState<TimeWithAvailabilityResponse[]>([]);
|
|
||||||
const [selectedTime, setSelectedTime] = useState<TimeWithAvailabilityResponse | null>(null);
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const handleError = (err: any) => {
|
|
||||||
if (isLoginRequiredError(err)) {
|
|
||||||
alert('로그인이 필요해요.');
|
|
||||||
navigate('/login', { state: { from: location } });
|
|
||||||
} else {
|
|
||||||
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
|
|
||||||
alert(message);
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchThemes().then(res => setThemes(res.themes)).catch(handleError);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedDate && selectedTheme) {
|
|
||||||
const dateStr = selectedDate.toLocaleDateString('en-CA');
|
|
||||||
fetchTimesWithAvailability(dateStr, selectedTheme.id)
|
|
||||||
.then(res => {
|
|
||||||
setTimes(res.times);
|
|
||||||
setSelectedTime(null);
|
|
||||||
})
|
|
||||||
.catch(handleError);
|
|
||||||
}
|
|
||||||
}, [selectedDate, selectedTheme]);
|
|
||||||
|
|
||||||
const handleNextStep = () => {
|
|
||||||
if (!selectedDate || !selectedTheme || !selectedTime) {
|
|
||||||
alert('날짜, 테마, 시간을 모두 선택해주세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedTime.isAvailable) {
|
|
||||||
alert('예약할 수 없는 시간입니다.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirmPayment = () => {
|
|
||||||
if (!selectedDate || !selectedTheme || !selectedTime) return;
|
|
||||||
|
|
||||||
const reservationData = {
|
|
||||||
date: selectedDate.toLocaleDateString('en-CA'),
|
|
||||||
themeId: selectedTheme.id,
|
|
||||||
timeId: selectedTime.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
createPendingReservation(reservationData)
|
|
||||||
.then((res) => {
|
|
||||||
navigate('/v2/reservation/payment', { state: { reservation: res } });
|
|
||||||
})
|
|
||||||
.catch(handleError)
|
|
||||||
.finally(() => setIsModalOpen(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
const isButtonDisabled = !selectedDate || !selectedTheme || !selectedTime || !selectedTime.isAvailable;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="content-container col-md-10 offset-md-1 p-5">
|
|
||||||
<h2 className="content-container-title">방탈출 예약</h2>
|
|
||||||
<div className="d-flex" id="reservation-container">
|
|
||||||
<div className="section border rounded col-md-4 p-3" id="date-section">
|
|
||||||
<h3 className="fs-5 text-center mb-3">날짜 선택</h3>
|
|
||||||
<div className="d-flex justify-content-center">
|
|
||||||
<Flatpickr
|
|
||||||
value={selectedDate || undefined}
|
|
||||||
onChange={([date]) => setSelectedDate(date)}
|
|
||||||
options={{ inline: true, defaultDate: new Date(), minDate: "today" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`section border rounded col-md-4 p-3 ${!selectedDate ? 'disabled' : ''}`} id="theme-section">
|
|
||||||
<h3 className="fs-5 text-center mb-3">테마 선택</h3>
|
|
||||||
<div className="p-3" id="theme-slots">
|
|
||||||
{themes.map(theme => (
|
|
||||||
<div key={theme.id}
|
|
||||||
className={`theme-slot cursor-pointer bg-light border rounded p-3 mb-2 ${selectedTheme?.id === theme.id ? 'active' : ''}`}
|
|
||||||
onClick={() => setSelectedTheme(theme)}>
|
|
||||||
{theme.name}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`section border rounded col-md-4 p-3 ${!selectedTheme ? 'disabled' : ''}`} id="time-section">
|
|
||||||
<h3 className="fs-5 text-center mb-3">시간 선택</h3>
|
|
||||||
<div className="p-3" id="time-slots">
|
|
||||||
{times.length > 0 ? times.map(time => (
|
|
||||||
<div key={time.id}
|
|
||||||
className={`time-slot cursor-pointer bg-light border rounded p-3 mb-2 ${selectedTime?.id === time.id ? 'active' : ''} ${!time.isAvailable ? 'disabled' : ''}`}
|
|
||||||
onClick={() => time.isAvailable && setSelectedTime(time)}>
|
|
||||||
{time.startAt} {!time.isAvailable && '(예약불가)'}
|
|
||||||
</div>
|
|
||||||
)) : <div className="no-times">선택할 수 있는 시간이 없습니다.</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="button-group float-end mt-3">
|
|
||||||
<button className="btn btn-primary" disabled={isButtonDisabled} onClick={handleNextStep}>
|
|
||||||
결제하기
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isModalOpen && (
|
|
||||||
<div className="modal-backdrop">
|
|
||||||
<div className="modal-dialog">
|
|
||||||
<div className="modal-content">
|
|
||||||
<div className="modal-header">
|
|
||||||
<h5 className="modal-title">예약 정보를 확인해주세요</h5>
|
|
||||||
<button type="button" className="btn-close" onClick={() => setIsModalOpen(false)}></button>
|
|
||||||
</div>
|
|
||||||
<div className="modal-body">
|
|
||||||
<p><strong>날짜:</strong> {selectedDate?.toLocaleDateString('ko-KR')}</p>
|
|
||||||
<p><strong>테마:</strong> {selectedTheme?.name}</p>
|
|
||||||
<p><strong>시간:</strong> {selectedTime?.startAt}</p>
|
|
||||||
</div>
|
|
||||||
<div className="modal-footer">
|
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => setIsModalOpen(false)}>취소</button>
|
|
||||||
<button type="button" className="btn btn-primary" onClick={handleConfirmPayment}>결제하기</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReservationStep1Page;
|
|
||||||
@ -1,120 +0,0 @@
|
|||||||
import { isLoginRequiredError } from '@_api/apiClient';
|
|
||||||
import { confirmReservationPayment } from '@_api/reservation/reservationAPI';
|
|
||||||
import { type ReservationCreateResponse, type ReservationPaymentRequest } from '@_api/reservation/reservationTypes';
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { PaymentType } from '@_api/payment/PaymentTypes';
|
|
||||||
import '@_css/reservation-v2.css';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
PaymentWidget: any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReservationStep2Page: React.FC = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
const paymentWidgetRef = useRef<any>(null);
|
|
||||||
const paymentMethodsRef = useRef<any>(null);
|
|
||||||
|
|
||||||
const reservation: ReservationCreateResponse | undefined = location.state?.reservation;
|
|
||||||
|
|
||||||
const handleError = (err: any) => {
|
|
||||||
if (isLoginRequiredError(err)) {
|
|
||||||
alert('로그인이 필요해요.');
|
|
||||||
navigate('/login', { state: { from: location } });
|
|
||||||
} else {
|
|
||||||
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
|
|
||||||
alert(message);
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!reservation) {
|
|
||||||
alert('잘못된 접근입니다.');
|
|
||||||
navigate('/v2/reservation');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = 'https://js.tosspayments.com/v1/payment-widget';
|
|
||||||
script.async = true;
|
|
||||||
document.head.appendChild(script);
|
|
||||||
|
|
||||||
script.onload = () => {
|
|
||||||
const widgetClientKey = "test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm";
|
|
||||||
const paymentWidget = window.PaymentWidget(widgetClientKey, window.PaymentWidget.ANONYMOUS);
|
|
||||||
paymentWidgetRef.current = paymentWidget;
|
|
||||||
|
|
||||||
const paymentMethods = paymentWidget.renderPaymentMethods(
|
|
||||||
"#payment-method",
|
|
||||||
{ value: 1000 }, // TODO: 테마별 요금 적용
|
|
||||||
{ variantKey: "DEFAULT" }
|
|
||||||
);
|
|
||||||
paymentMethodsRef.current = paymentMethods;
|
|
||||||
};
|
|
||||||
}, [reservation, navigate]);
|
|
||||||
|
|
||||||
const handlePayment = () => {
|
|
||||||
if (!paymentWidgetRef.current || !reservation) {
|
|
||||||
alert('결제 위젯이 로드되지 않았거나 예약 정보가 없습니다.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateRandomString = () =>
|
|
||||||
crypto.randomUUID().replace(/-/g, '');
|
|
||||||
|
|
||||||
paymentWidgetRef.current.requestPayment({
|
|
||||||
orderId: generateRandomString(),
|
|
||||||
orderName: "테스트 방탈출 예약 결제 1건",
|
|
||||||
amount: 1000,
|
|
||||||
}).then((data: any) => {
|
|
||||||
const paymentData: ReservationPaymentRequest = {
|
|
||||||
paymentKey: data.paymentKey,
|
|
||||||
orderId: data.orderId,
|
|
||||||
amount: data.amount,
|
|
||||||
paymentType: data.paymentType || PaymentType.NORMAL,
|
|
||||||
};
|
|
||||||
confirmReservationPayment(reservation.reservationId, paymentData)
|
|
||||||
.then((res) => {
|
|
||||||
navigate('/v2/reservation/success', {
|
|
||||||
state: {
|
|
||||||
reservation: res,
|
|
||||||
themeName: reservation.themeName,
|
|
||||||
date: reservation.date,
|
|
||||||
startAt: reservation.startAt,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(handleError);
|
|
||||||
}).catch((error: any) => {
|
|
||||||
console.error("Payment request error:", error);
|
|
||||||
alert("결제 요청 중 오류가 발생했습니다.");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!reservation) {
|
|
||||||
return null; // or a loading spinner
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="content-container col-md-10 offset-md-1 p-5">
|
|
||||||
<div className="wrapper w-100">
|
|
||||||
<div className="max-w-540 w-100">
|
|
||||||
<div id="payment-method" className="w-100"></div>
|
|
||||||
<div id="agreement" className="w-100"></div>
|
|
||||||
<div className="btn-wrapper w-100 mt-3">
|
|
||||||
<button onClick={handlePayment} className="btn btn-primary w-100">
|
|
||||||
1,000원 결제하기
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReservationStep2Page;
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useLocation, useNavigate, Link } from 'react-router-dom';
|
|
||||||
import type { ReservationPaymentResponse } from '@_api/reservation/reservationTypes';
|
|
||||||
import '@_css/reservation-v2.css';
|
|
||||||
|
|
||||||
const ReservationSuccessPage: React.FC = () => {
|
|
||||||
const location = useLocation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { reservation, themeName, date, startAt } = (location.state as {
|
|
||||||
reservation: ReservationPaymentResponse;
|
|
||||||
themeName: string;
|
|
||||||
date: string;
|
|
||||||
startAt: string;
|
|
||||||
}) || {};
|
|
||||||
|
|
||||||
if (!reservation) {
|
|
||||||
React.useEffect(() => {
|
|
||||||
navigate('/');
|
|
||||||
}, [navigate]);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="reservation-success-page">
|
|
||||||
<h2 className="content-container-title">예약이 확정되었습니다!</h2>
|
|
||||||
<div className="reservation-info-box">
|
|
||||||
<h3>최종 예약 정보</h3>
|
|
||||||
<div className="info-item"><strong>테마:</strong> <span>{themeName}</span></div>
|
|
||||||
<div className="info-item"><strong>날짜:</strong> <span>{date}</span></div>
|
|
||||||
<div className="info-item"><strong>시간:</strong> <span>{startAt}</span></div>
|
|
||||||
</div>
|
|
||||||
<div className="success-page-actions">
|
|
||||||
<Link to="/my-reservation/v2" className="btn btn-secondary">
|
|
||||||
내 예약 목록
|
|
||||||
</Link>
|
|
||||||
<Link to="/" className="btn btn-secondary">
|
|
||||||
메인 페이지로 이동
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReservationSuccessPage;
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { signup } from '../../api/member/memberAPI';
|
|
||||||
import type { SignupRequest } from '../../api/member/memberTypes';
|
|
||||||
import '../../css/signup-page-v2.css';
|
|
||||||
|
|
||||||
const SignupPageV2: React.FC = () => {
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [name, setName] = useState('');
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleSignup = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const request: SignupRequest = { email, password, name };
|
|
||||||
try {
|
|
||||||
const response = await signup(request);
|
|
||||||
alert(`${response.name}님, 회원가입을 축하드려요. 로그인 후 이용해주세요!`);
|
|
||||||
navigate('/v2/login');
|
|
||||||
} catch (error: any) {
|
|
||||||
const message = error.response?.data?.message || '회원가입에 실패했어요. 입력 정보를 확인해주세요.';
|
|
||||||
alert(message);
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="signup-container-v2">
|
|
||||||
<h2 className="page-title">회원가입</h2>
|
|
||||||
<form className="signup-form-v2" onSubmit={handleSignup}>
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">이메일</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
className="form-input"
|
|
||||||
placeholder="이메일을 입력하세요"
|
|
||||||
value={email}
|
|
||||||
onChange={e => setEmail(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">비밀번호</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
className="form-input"
|
|
||||||
placeholder="비밀번호를 입력하세요"
|
|
||||||
value={password}
|
|
||||||
onChange={e => setPassword(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">이름</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-input"
|
|
||||||
placeholder="이름을 입력하세요"
|
|
||||||
value={name}
|
|
||||||
onChange={e => setName(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button type="submit" className="btn-primary">가입하기</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SignupPageV2;
|
|
||||||
@ -29,6 +29,7 @@
|
|||||||
"@_api/*": ["src/api/*"],
|
"@_api/*": ["src/api/*"],
|
||||||
"@_assets/*": ["src/assets/*"],
|
"@_assets/*": ["src/assets/*"],
|
||||||
"@_components/*": ["src/components/*"],
|
"@_components/*": ["src/components/*"],
|
||||||
|
"@_context/*": ["src/context/*"],
|
||||||
"@_css/*": ["src/css/*"],
|
"@_css/*": ["src/css/*"],
|
||||||
"@_hooks/*": ["src/hooks/*"],
|
"@_hooks/*": ["src/hooks/*"],
|
||||||
"@_pages/*": ["src/pages/*"],
|
"@_pages/*": ["src/pages/*"],
|
||||||
|
|||||||
@ -66,13 +66,15 @@ class PaymentService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun findDetailByReservationId(reservationId: Long): PaymentRetrieveResponse {
|
fun findDetailByReservationId(reservationId: Long): PaymentRetrieveResponse? {
|
||||||
val payment: PaymentEntity = findByReservationIdOrThrow(reservationId)
|
log.info { "[PaymentService.findDetailByReservationId] 예약 결제 정보 조회 시작: reservationId=$reservationId" }
|
||||||
val paymentDetail: PaymentDetailEntity = findDetailByPaymentIdOrThrow(payment.id)
|
|
||||||
val cancelDetail: CanceledPaymentEntity? = canceledPaymentRepository.findByPaymentId(payment.id)
|
|
||||||
|
|
||||||
return payment.toRetrieveResponse(
|
val payment: PaymentEntity? = findByReservationIdOrNull(reservationId)
|
||||||
detail = paymentDetail.toPaymentDetailResponse(),
|
val paymentDetail: PaymentDetailEntity? = payment?.let { findDetailByPaymentIdOrNull(it.id) }
|
||||||
|
val cancelDetail: CanceledPaymentEntity? = payment?.let { findCancelByPaymentIdOrNull(it.id) }
|
||||||
|
|
||||||
|
return payment?.toRetrieveResponse(
|
||||||
|
detail = paymentDetail?.toPaymentDetailResponse(),
|
||||||
cancel = cancelDetail?.toCancelDetailResponse()
|
cancel = cancelDetail?.toCancelDetailResponse()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -88,14 +90,40 @@ class PaymentService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findDetailByPaymentIdOrThrow(paymentId: Long): PaymentDetailEntity {
|
private fun findByReservationIdOrNull(reservationId: Long): PaymentEntity? {
|
||||||
log.info { "[PaymentService.findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 시작: paymentId=$paymentId" }
|
log.info { "[PaymentService.findByReservationIdOrThrow] 결제 정보 조회 시작: reservationId=: $reservationId" }
|
||||||
|
|
||||||
return paymentDetailRepository.findByPaymentId(paymentId)
|
return paymentRepository.findByReservationId(reservationId)
|
||||||
?.also { log.info { "[PaymentService.findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 완료: paymentId=$paymentId, detailId=${it.id}}" } }
|
.also {
|
||||||
?: run {
|
if (it != null) {
|
||||||
log.warn { "[PaymentService.findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 실패: paymentId=$paymentId" }
|
log.info { "[PaymentService.findByReservationIdOrThrow] 결제 정보 조회 완료: reservationId=$reservationId, paymentId=${it.id}" }
|
||||||
throw PaymentException(PaymentErrorCode.PAYMENT_DETAIL_NOT_FOUND)
|
} else {
|
||||||
|
log.warn { "[PaymentService.findByReservationIdOrThrow] 결제 정보 조회 실패: reservationId=$reservationId" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun findDetailByPaymentIdOrNull(paymentId: Long): PaymentDetailEntity? {
|
||||||
|
log.info { "[PaymentService.findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 시작: paymentId=$paymentId" }
|
||||||
|
|
||||||
|
return paymentDetailRepository.findByPaymentId(paymentId).also {
|
||||||
|
if (it != null) {
|
||||||
|
log.info { "[PaymentService.findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 완료: paymentId=$paymentId, detailId=${it.id}}" }
|
||||||
|
} else {
|
||||||
|
log.warn { "[PaymentService.findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 실패: paymentId=$paymentId" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findCancelByPaymentIdOrNull(paymentId: Long): CanceledPaymentEntity? {
|
||||||
|
log.info { "[PaymentService.findDetailByReservationId] 취소 결제 정보 조회 시작: paymentId=${paymentId}" }
|
||||||
|
|
||||||
|
return canceledPaymentRepository.findByPaymentId(paymentId).also {
|
||||||
|
if (it == null) {
|
||||||
|
log.info { "[PaymentService.findDetailByReservationId] 취소 결제 정보가 없음: paymentId=${paymentId}" }
|
||||||
|
} else {
|
||||||
|
log.info { "[PaymentService.findDetailByReservationId] 취소 결제 정보 조회 완료: paymentId=${paymentId}, cancelId=${it.id}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user