From ef58752cec68973d369c962202ee5fdcc4e1f3fa Mon Sep 17 00:00:00 2001 From: pricelees Date: Fri, 22 Aug 2025 06:43:16 +0000 Subject: [PATCH] =?UTF-8?q?[#35]=20=EA=B2=B0=EC=A0=9C=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=20=EC=9E=AC=EC=A0=95=EC=9D=98=20&=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=20=EC=A1=B0=ED=9A=8C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📝 관련 이슈 및 PR **PR과 관련된 이슈 번호** - #35 ## ✨ 작업 내용 - 운영을 고려하여 조금 더 디테일한 정보가 담기도록 결제 스키마 개선(결제수단, 금액, 카드 사용시 카드번호, 할부 정보 등) - 회원의 예약 조회 페이지 개선 및 회원의 예약 취소 기능 도입 ## 🧪 테스트 - 현재 테스트가 과연 신뢰성이 있는가 의문. 추후 전체적인 작업 후 전체 테스트를 재조정할 예정 ## 📚 참고 자료 및 기타 Reviewed-on: https://gitea.pricelees.me/pricelees/roomescape-refactored/pulls/36 Co-authored-by: pricelees Co-committed-by: pricelees --- build.gradle.kts | 4 + frontend/src/App.css | 8 +- frontend/src/App.tsx | 14 +- frontend/src/api/member/memberTypes.ts | 4 +- .../src/api/reservation/reservationAPI.ts | 39 +- .../src/api/reservation/reservationAPIV2.ts | 0 .../src/api/reservation/reservationTypes.ts | 159 ++++++++- frontend/src/api/theme/themeAPI.ts | 2 +- frontend/src/api/theme/themeTypes.ts | 4 +- frontend/src/api/time/timeAPI.ts | 4 +- frontend/src/api/time/timeTypes.ts | 6 +- frontend/src/components/Navbar.tsx | 4 +- frontend/src/css/my-reservation-v2.css | 288 +++++++++++++++ frontend/src/css/reservation-v2.css | 175 +++++++++ frontend/src/index.css | 18 +- frontend/src/pages/MyReservationPage.tsx | 6 +- frontend/src/pages/ReservationPage.tsx | 9 +- frontend/src/pages/admin/ReservationPage.tsx | 12 +- frontend/src/pages/admin/ThemePage.tsx | 2 +- frontend/src/pages/admin/TimePage.tsx | 2 +- frontend/src/pages/admin/WaitingPage.tsx | 4 +- frontend/src/pages/v2/MyReservationPageV2.tsx | 337 ++++++++++++++++++ .../src/pages/v2/ReservationStep1Page.tsx | 156 ++++++++ .../src/pages/v2/ReservationStep2Page.tsx | 118 ++++++ .../src/pages/v2/ReservationSuccessPage.tsx | 44 +++ .../roomescape/common/config/JacksonConfig.kt | 80 ++++- .../roomescape/common/entity/BaseEntity.kt | 19 + .../common/log/ControllerLoggingAspect.kt | 17 +- .../common/util/TransactionExecutionUtil.kt | 31 ++ .../payment/exception/PaymentErrorCode.kt | 11 +- .../payment/implement/PaymentFinderV2.kt | 53 +++ .../payment/implement/PaymentRequester.kt | 21 ++ .../payment/implement/PaymentWriterV2.kt | 80 +++++ .../infrastructure/client/TossPaymentDTO.kt | 2 - .../client/v2/TosspaymentCancelDTO.kt | 74 ++++ .../client/v2/TosspaymentClientV2.kt | 136 +++++++ .../client/v2/TosspaymentConfirmDTO.kt | 124 +++++++ .../infrastructure/common/PaymentTypes.kt | 243 +++++++++++++ .../persistence/v2/CanceledPaymentEntityV2.kt | 24 ++ .../v2/CanceledPaymentRepositoryV2.kt | 7 + .../persistence/v2/PaymentDetailEntity.kt | 79 ++++ .../persistence/v2/PaymentDetailRepository.kt | 7 + .../persistence/v2/PaymentEntityV2.kt | 38 ++ .../persistence/v2/PaymentRepositoryV2.kt | 8 + .../business/MyReservationFindService.kt | 54 +++ .../ReservationWithPaymentServiceV2.kt | 108 ++++++ .../reservation/docs/MyReservationAPI.kt | 33 ++ .../docs/ReservationWithPaymentAPI.kt | 61 ++++ .../exception/ReservationErrorCode.kt | 2 + .../implement/ReservationFinder.kt | 19 + .../implement/ReservationValidator.kt | 34 ++ .../implement/ReservationWriter.kt | 27 ++ .../persistence/ReservationEntity.kt | 11 + .../persistence/ReservationRepository.kt | 2 + .../web/MyReservationController.kt | 35 ++ .../reservation/web/MyReservationResponse.kt | 196 ++++++++++ .../web/ReservationWithPaymentController.kt | 60 ++++ .../web/ReservationWithPaymentDTO.kt | 57 +++ .../roomescape/time/implement/TimeFinder.kt | 2 +- src/main/resources/schema/region-data.sql | 2 +- src/main/resources/schema/schema-h2.sql | 98 ++++- src/main/resources/schema/schema-mysql.sql | 82 +++++ src/main/resources/test.http | 4 + .../common/config/JacksonConfigTest.kt | 37 ++ .../web/ReservationControllerTest.kt | 2 +- .../time/implement/TimeFinderTest.kt | 35 ++ 66 files changed, 3338 insertions(+), 96 deletions(-) create mode 100644 frontend/src/api/reservation/reservationAPIV2.ts create mode 100644 frontend/src/css/my-reservation-v2.css create mode 100644 frontend/src/css/reservation-v2.css create mode 100644 frontend/src/pages/v2/MyReservationPageV2.tsx create mode 100644 frontend/src/pages/v2/ReservationStep1Page.tsx create mode 100644 frontend/src/pages/v2/ReservationStep2Page.tsx create mode 100644 frontend/src/pages/v2/ReservationSuccessPage.tsx create mode 100644 src/main/kotlin/roomescape/common/util/TransactionExecutionUtil.kt create mode 100644 src/main/kotlin/roomescape/payment/implement/PaymentFinderV2.kt create mode 100644 src/main/kotlin/roomescape/payment/implement/PaymentRequester.kt create mode 100644 src/main/kotlin/roomescape/payment/implement/PaymentWriterV2.kt create mode 100644 src/main/kotlin/roomescape/payment/infrastructure/client/v2/TosspaymentCancelDTO.kt create mode 100644 src/main/kotlin/roomescape/payment/infrastructure/client/v2/TosspaymentClientV2.kt create mode 100644 src/main/kotlin/roomescape/payment/infrastructure/client/v2/TosspaymentConfirmDTO.kt create mode 100644 src/main/kotlin/roomescape/payment/infrastructure/common/PaymentTypes.kt create mode 100644 src/main/kotlin/roomescape/payment/infrastructure/persistence/v2/CanceledPaymentEntityV2.kt create mode 100644 src/main/kotlin/roomescape/payment/infrastructure/persistence/v2/CanceledPaymentRepositoryV2.kt create mode 100644 src/main/kotlin/roomescape/payment/infrastructure/persistence/v2/PaymentDetailEntity.kt create mode 100644 src/main/kotlin/roomescape/payment/infrastructure/persistence/v2/PaymentDetailRepository.kt create mode 100644 src/main/kotlin/roomescape/payment/infrastructure/persistence/v2/PaymentEntityV2.kt create mode 100644 src/main/kotlin/roomescape/payment/infrastructure/persistence/v2/PaymentRepositoryV2.kt create mode 100644 src/main/kotlin/roomescape/reservation/business/MyReservationFindService.kt create mode 100644 src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceV2.kt create mode 100644 src/main/kotlin/roomescape/reservation/docs/MyReservationAPI.kt create mode 100644 src/main/kotlin/roomescape/reservation/docs/ReservationWithPaymentAPI.kt create mode 100644 src/main/kotlin/roomescape/reservation/web/MyReservationController.kt create mode 100644 src/main/kotlin/roomescape/reservation/web/MyReservationResponse.kt create mode 100644 src/main/kotlin/roomescape/reservation/web/ReservationWithPaymentController.kt create mode 100644 src/main/kotlin/roomescape/reservation/web/ReservationWithPaymentDTO.kt create mode 100644 src/main/resources/test.http diff --git a/build.gradle.kts b/build.gradle.kts index bfec98c1..8405fd6f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,6 +21,10 @@ java { } } +tasks.jar { + enabled = false +} + kapt { keepJavacAnnotationProcessors = true } diff --git a/frontend/src/App.css b/frontend/src/App.css index b9d355df..1afa855c 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -33,10 +33,6 @@ } } -.card { - padding: 2em; -} -.read-the-docs { - color: #888; -} + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 225f6538..d31faf7f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,6 +13,10 @@ import AdminThemePage from './pages/admin/ThemePage'; import AdminWaitingPage from './pages/admin/WaitingPage'; import { AuthProvider } from './context/AuthContext'; import AdminRoute from './components/AdminRoute'; +import ReservationStep1Page from './pages/v2/ReservationStep1Page'; +import ReservationStep2Page from './pages/v2/ReservationStep2Page'; +import ReservationSuccessPage from './pages/v2/ReservationSuccessPage'; +import MyReservationPageV2 from './pages/v2/MyReservationPageV2'; const AdminRoutes = () => ( @@ -43,7 +47,13 @@ function App() { } /> } /> } /> - } /> + } /> + } /> + + {/* V2 Reservation Flow */} + } /> + } /> + } /> } /> @@ -53,4 +63,4 @@ function App() { ); } -export default App; +export default App; \ No newline at end of file diff --git a/frontend/src/api/member/memberTypes.ts b/frontend/src/api/member/memberTypes.ts index 45d90e86..32d4ab17 100644 --- a/frontend/src/api/member/memberTypes.ts +++ b/frontend/src/api/member/memberTypes.ts @@ -1,5 +1,5 @@ export interface MemberRetrieveResponse { - id: number; + id: string; name: string; } @@ -14,6 +14,6 @@ export interface SignupRequest { } export interface SignupResponse { - id: number; + id: string; name: string; } diff --git a/frontend/src/api/reservation/reservationAPI.ts b/frontend/src/api/reservation/reservationAPI.ts index 276ce06f..01a39c4a 100644 --- a/frontend/src/api/reservation/reservationAPI.ts +++ b/frontend/src/api/reservation/reservationAPI.ts @@ -2,10 +2,16 @@ import apiClient from "@_api/apiClient"; import type { AdminReservationCreateRequest, MyReservationRetrieveListResponse, + ReservationCreateRequest, + ReservationCreateResponse, ReservationCreateWithPaymentRequest, + ReservationDetailV2, + ReservationPaymentRequest, + ReservationPaymentResponse, ReservationRetrieveListResponse, ReservationRetrieveResponse, ReservationSearchQuery, + ReservationSummaryListV2, WaitingCreateRequest } from "./reservationTypes"; @@ -30,7 +36,7 @@ export const searchReservations = async (params: ReservationSearchQuery): Promis }; // DELETE /reservations/{id} -export const cancelReservationByAdmin = async (id: number): Promise => { +export const cancelReservationByAdmin = async (id: string): Promise => { return await apiClient.del(`/reservations/${id}`, true); }; @@ -55,16 +61,41 @@ export const createWaiting = async (data: WaitingCreateRequest): Promise => { +export const cancelWaiting = async (id: string): Promise => { return await apiClient.del(`/reservations/waiting/${id}`, true); }; // POST /reservations/waiting/{id}/confirm -export const confirmWaiting = async (id: number): Promise => { +export const confirmWaiting = async (id: string): Promise => { return await apiClient.post(`/reservations/waiting/${id}/confirm`, {}, true); }; // POST /reservations/waiting/{id}/reject -export const rejectWaiting = async (id: number): Promise => { +export const rejectWaiting = async (id: string): Promise => { return await apiClient.post(`/reservations/waiting/${id}/reject`, {}, true); }; + +// POST /v2/reservations +export const createPendingReservation = async (data: ReservationCreateRequest): Promise => { + return await apiClient.post('/v2/reservations', data, true); +}; + +// POST /v2/reservations/{id}/pay +export const confirmReservationPayment = async (id: string, data: ReservationPaymentRequest): Promise => { + return await apiClient.post(`/v2/reservations/${id}/pay`, data, true); +}; + +// POST /v2/reservations/{id}/cancel +export const cancelReservationV2 = async (id: string, cancelReason: string): Promise => { + return await apiClient.post(`/v2/reservations/${id}/cancel`, { cancelReason }, true); +}; + +// GET /v2/reservations +export const fetchMyReservationsV2 = async (): Promise => { + return await apiClient.get('/v2/reservations', true); +}; + +// GET /v2/reservations/{id}/details +export const fetchReservationDetailV2 = async (id: string): Promise => { + return await apiClient.get(`/v2/reservations/${id}/details`, true); +}; \ No newline at end of file diff --git a/frontend/src/api/reservation/reservationAPIV2.ts b/frontend/src/api/reservation/reservationAPIV2.ts new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/api/reservation/reservationTypes.ts b/frontend/src/api/reservation/reservationTypes.ts index 5bffd87f..5501ed8c 100644 --- a/frontend/src/api/reservation/reservationTypes.ts +++ b/frontend/src/api/reservation/reservationTypes.ts @@ -3,18 +3,24 @@ 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.WAITING + | typeof ReservationStatus.CANCELED_BY_USER + | typeof ReservationStatus.AUTOMATICALLY_CANCELED; export interface MyReservationRetrieveResponse { - id: number; + id: string; themeName: string; date: string; time: string; @@ -29,7 +35,7 @@ export interface MyReservationRetrieveListResponse { } export interface ReservationRetrieveResponse { - id: number; + id: string; date: string; member: MemberRetrieveResponse; time: TimeRetrieveResponse; @@ -43,15 +49,15 @@ export interface ReservationRetrieveListResponse { export interface AdminReservationCreateRequest { date: string; - timeId: number; - themeId: number; - memberId: number; + timeId: string; + themeId: string; + memberId: string; } export interface ReservationCreateWithPaymentRequest { date: string; - timeId: number; - themeId: number; + timeId: string; + themeId: string; paymentKey: string; orderId: string; amount: number; @@ -60,13 +66,142 @@ export interface ReservationCreateWithPaymentRequest { export interface WaitingCreateRequest { date: string; - timeId: number; - themeId: number; + timeId: string; + themeId: string; } export interface ReservationSearchQuery { - themeId?: number; - memberId?: number; + themeId?: string; + memberId?: string; dateFrom?: string; dateTo?: string; } + +// V2 types +export const PaymentType = { + NORMAL: 'NORMAL', + BILLING: 'BILLING', + BRANDPAY: 'BRANDPAY' +} as const; + +export type PaymentType = + | typeof PaymentType.NORMAL + | typeof PaymentType.BILLING + | typeof PaymentType.BRANDPAY; + +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 ReservationSummaryV2 { + id: string; + themeName: string; + date: string; + startAt: string; + status: string; // 'CONFIRMED', 'CANCELED_BY_USER', etc. +} + +export interface ReservationSummaryListV2 { + reservations: ReservationSummaryV2[]; +} + +export interface ReservationDetailV2 { + id: string; + user: UserDetailV2; + themeName: string; + date: string; + startAt: string; + applicationDateTime: string; + payment: PaymentV2; + cancellation: CancellationV2 | null; +} + +export interface UserDetailV2 { + id: string; + name: string; + email: string; +} + +export interface PaymentV2 { + orderId: string; + totalAmount: number; + method: string; + status: 'DONE' | 'CANCELED'; + requestedAt: string; + approvedAt: string; + detail: CardPaymentDetailV2 | BankTransferPaymentDetailV2 | EasyPayPrepaidPaymentDetailV2; +} + +export interface CardPaymentDetailV2 { + type: 'CARD'; + issuerCode: string; + cardType: 'CREDIT' | 'CHECK' | 'GIFT'; + ownerType: 'PERSONAL' | 'CORPORATE'; + cardNumber: string; + amount: number; + approvalNumber: string; + installmentPlanMonths: number; + isInterestFree: boolean; + easypayProviderName?: string; + easypayDiscountAmount?: number; +} + +export interface BankTransferPaymentDetailV2 { + type: 'BANK_TRANSFER'; + bankName: string; + settlementStatus: string; +} + +export interface EasyPayPrepaidPaymentDetailV2 { + type: 'EASYPAY_PREPAID'; + providerName: string; + amount: number; + discountAmount: number; +} + +export interface CancellationV2 { + cancellationRequestedAt: string; // ISO 8601 format + cancellationApprovedAt: string; // ISO 8601 format + cancelReason: string; + canceledBy: string; +} diff --git a/frontend/src/api/theme/themeAPI.ts b/frontend/src/api/theme/themeAPI.ts index c653a9e8..6cbe5c3d 100644 --- a/frontend/src/api/theme/themeAPI.ts +++ b/frontend/src/api/theme/themeAPI.ts @@ -13,6 +13,6 @@ export const mostReservedThemes = async (count: number = 10): Promise(`/themes/most-reserved-last-week?count=${count}`, false); }; -export const delTheme = async (id: number): Promise => { +export const delTheme = async (id: string): Promise => { return await apiClient.del(`/themes/${id}`, true); }; diff --git a/frontend/src/api/theme/themeTypes.ts b/frontend/src/api/theme/themeTypes.ts index d1721953..129f1fc1 100644 --- a/frontend/src/api/theme/themeTypes.ts +++ b/frontend/src/api/theme/themeTypes.ts @@ -5,14 +5,14 @@ export interface ThemeCreateRequest { } export interface ThemeCreateResponse { - id: number; + id: string; name: string; description: string; thumbnail: string; } export interface ThemeRetrieveResponse { - id: number; + id: string; name: string; description: string; thumbnail: string; diff --git a/frontend/src/api/time/timeAPI.ts b/frontend/src/api/time/timeAPI.ts index 2a2d6ac2..656f90e9 100644 --- a/frontend/src/api/time/timeAPI.ts +++ b/frontend/src/api/time/timeAPI.ts @@ -9,10 +9,10 @@ export const fetchTimes = async (): Promise => { return await apiClient.get('/times', true); }; -export const delTime = async (id: number): Promise => { +export const delTime = async (id: string): Promise => { return await apiClient.del(`/times/${id}`, true); }; -export const fetchTimesWithAvailability = async (date: string, themeId: number): Promise => { +export const fetchTimesWithAvailability = async (date: string, themeId: string): Promise => { return await apiClient.get(`/times/search?date=${date}&themeId=${themeId}`, true); }; diff --git a/frontend/src/api/time/timeTypes.ts b/frontend/src/api/time/timeTypes.ts index 408e8f7f..acb7c350 100644 --- a/frontend/src/api/time/timeTypes.ts +++ b/frontend/src/api/time/timeTypes.ts @@ -3,12 +3,12 @@ export interface TimeCreateRequest { } export interface TimeCreateResponse { - id: number; + id: string; startAt: string; } export interface TimeRetrieveResponse { - id: number; + id: string; startAt: string; } @@ -17,7 +17,7 @@ export interface TimeRetrieveListResponse { } export interface TimeWithAvailabilityResponse { - id: number; + id: string; startAt: string; isAvailable: boolean; } diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 6d6a324d..8644022f 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -27,7 +27,7 @@ const Navbar: React.FC = () => {