diff --git a/README.md b/README.md index 4370790e0..779637b67 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,7 @@ - [x] 예약 생성 전 결제 외부 API 호출 - [x] 예약 실패 시 예외 처리 및 실패 사유 전달 +### 2단계 구현 기능 목록 +- [x] 내 예약 페이지 결제 정보 추가 + - [x] paymentKey, 결제 금액 조회 + - [x] 그 외 결제 정보 DB에 선택적으로 저장 diff --git a/build.gradle b/build.gradle index e8b80b8be..88ff95c0f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ plugins { id 'org.springframework.boot' version '3.2.4' id 'io.spring.dependency-management' version '1.1.4' + id 'org.asciidoctor.jvm.convert' version '3.3.2' id 'java' } @@ -8,6 +9,10 @@ group = 'nextstep' version = '0.0.1-SNAPSHOT' sourceCompatibility = '17' +configurations { + asciidoctorExt +} + repositories { mavenCentral() } @@ -25,9 +30,36 @@ dependencies { runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' testImplementation 'io.rest-assured:rest-assured:5.3.1' + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' +} + +ext { + snippetsDir = file('build/generated-snippets') } test { useJUnitPlatform() + outputs.dir snippetsDir +} + +asciidoctor { + inputs.dir snippetsDir + configurations 'asciidoctorExt' + dependsOn test +} + +asciidoctor.doFirst { + delete file('src/main/resources/static/docs') +} + +tasks.register('copyDocument', Copy) { + dependsOn asciidoctor + from file("${asciidoctor.outputDir}") + into file("src/main/resources/static/docs") +} + +build { + dependsOn copyDocument } diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc new file mode 100644 index 000000000..09536e2a6 --- /dev/null +++ b/src/docs/asciidoc/index.adoc @@ -0,0 +1,82 @@ +ifndef::snippets[] +:snippets: ./build/generated-snippets +endif::[] + + +== 1️⃣ 회원 +:domain: member +:class-name: member-controller-test + +=== 조회 +operation::{class-name}/show-{domain}[snippets='request-cookies,response-fields,http-request,http-response'] + +=== 저장 +operation::{class-name}/save-{domain}[snippets='request-fields,http-request,http-response'] + +=== 삭제 +operation::{class-name}/delete-{domain}[snippets='request-cookies,http-request,http-response'] + +== 2️⃣ 인증 +:domain: auth +:class-name: login-controller-test + +=== 로그인 +operation::{class-name}/login[snippets='request-fields,http-request,http-response'] + +=== 로그아웃 +operation::{class-name}/logout[snippets='request-cookies,http-request,http-response'] + +=== 로그인 여부 확인 +operation::{class-name}/login-check[snippets='request-cookies,response-fields,http-request,http-response'] + +== 3️⃣ 예약 시간 +:domain: reservation-time +:class-name: reservation-time-controller-test + +=== 조회 +operation::{class-name}/show-{domain}[snippets='request-cookies,response-fields,http-request,http-response'] + +=== 저장 +operation::{class-name}/save-{domain}[snippets='request-cookies,request-fields,response-fields,http-request,http-response'] + +=== 삭제 +operation::{class-name}/delete-{domain}[snippets='request-cookies,http-request,http-response'] + +== 4️⃣ 테마 +:domain: theme +:class-name: theme-controller-test + +=== 조회 +operation::{class-name}/show-{domain}[snippets='request-cookies,response-fields,http-request,http-response'] + +=== 저장 +operation::{class-name}/save-{domain}[snippets='request-cookies,request-fields,response-fields,http-request,http-response'] + +=== 삭제 +operation::{class-name}/delete-{domain}[snippets='request-cookies,http-request,http-response'] + +=== 인기 조회 +operation::{class-name}/show-popular-theme[snippets='response-fields,http-request,http-response'] + +== 5️⃣ 예약 +:domain: reservation +:class-name: reservation-controller-test + +=== 조회 +operation::{class-name}/show-{domain}[snippets='request-cookies,response-fields,http-request,http-response'] + +=== 저장 +operation::{class-name}/save-{domain}[snippets='request-cookies,request-fields,response-fields,http-request,http-response'] + +=== 삭제 +operation::{class-name}/delete-{domain}[snippets='request-cookies,http-request,http-response'] + +=== 검색 (필터링) +operation::admin-reservation-controller-test/find-reservation-by-filter[snippets='request-cookies,response-fields,http-request,http-response'] + +== 6️⃣ 대기 +:domain: waiting +:class-name: reservation-controller-test + +=== 조회 +operation::admin-reservation-controller-test/find-all-waiting[snippets='request-cookies,response-fields,http-request,http-response'] diff --git a/src/main/java/roomescape/config/WebMvcConfiguration.java b/src/main/java/roomescape/config/WebMvcConfiguration.java index d228fad6f..fbfdb0f46 100644 --- a/src/main/java/roomescape/config/WebMvcConfiguration.java +++ b/src/main/java/roomescape/config/WebMvcConfiguration.java @@ -34,7 +34,8 @@ public void addInterceptors(InterceptorRegistry registry) { .addPathPatterns("/**") .excludePathPatterns("/", "/error", "/login", "/signup", "/members", "/themes/popular", - "/css/**", "/*.ico", "/js/**", "/image/**"); + "/css/**", "/*.ico", "/js/**", "/image/**", + "/docs"); registry.addInterceptor(new CheckAdminInterceptor()) .order(2) diff --git a/src/main/java/roomescape/controller/LoginController.java b/src/main/java/roomescape/controller/LoginController.java index b05814373..74470b4cb 100644 --- a/src/main/java/roomescape/controller/LoginController.java +++ b/src/main/java/roomescape/controller/LoginController.java @@ -25,11 +25,6 @@ public LoginController(MemberService memberService, AuthService authService) { this.authService = authService; } - @GetMapping("/login") - public String page() { - return "login"; - } - @PostMapping("/login") public void login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) { MemberResponse memberResponse = memberService.findByEmailAndPassword(loginRequest.email(), loginRequest.password()); diff --git a/src/main/java/roomescape/controller/PageController.java b/src/main/java/roomescape/controller/PageController.java new file mode 100644 index 000000000..728eff367 --- /dev/null +++ b/src/main/java/roomescape/controller/PageController.java @@ -0,0 +1,38 @@ +package roomescape.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class PageController { + + @GetMapping + public String home() { + return "index"; + } + + @GetMapping("/login") + public String login() { + return "login"; + } + + @GetMapping("/signup") + public String signUp() { + return "signup"; + } + + @GetMapping("/reservation") + public String reservation() { + return "reservation"; + } + + @GetMapping("/reservation-mine") + public String reservationMine() { + return "reservation-mine"; + } + + @GetMapping("/reservation/payment") + public String reservationPayment() { + return "reservation-payment"; + } +} diff --git a/src/main/java/roomescape/controller/UserPageController.java b/src/main/java/roomescape/controller/UserPageController.java deleted file mode 100644 index e054bbfe6..000000000 --- a/src/main/java/roomescape/controller/UserPageController.java +++ /dev/null @@ -1,13 +0,0 @@ -package roomescape.controller; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; - -@Controller -public class UserPageController { - - @GetMapping("/reservation-mine") - public String reservationMine() { - return "reservation-mine"; - } -} diff --git a/src/main/java/roomescape/controller/admin/AdminPageController.java b/src/main/java/roomescape/controller/admin/AdminPageController.java index 3e43b1ffe..0a2519398 100644 --- a/src/main/java/roomescape/controller/admin/AdminPageController.java +++ b/src/main/java/roomescape/controller/admin/AdminPageController.java @@ -10,26 +10,26 @@ public class AdminPageController { @GetMapping public String home() { - return "/admin/index"; + return "admin/index"; } @GetMapping("/reservation") public String reservation() { - return "/admin/reservation-new"; + return "admin/reservation-new"; } @GetMapping("/time") public String time() { - return "/admin/time"; + return "admin/time"; } @GetMapping("/theme") public String theme() { - return "/admin/theme"; + return "admin/theme"; } @GetMapping("/waiting") public String waiting() { - return "/admin/waiting"; + return "admin/waiting"; } } diff --git a/src/main/java/roomescape/domain/payment/PaymentFailure.java b/src/main/java/roomescape/domain/payment/PaymentFailure.java new file mode 100644 index 000000000..675c3cee0 --- /dev/null +++ b/src/main/java/roomescape/domain/payment/PaymentFailure.java @@ -0,0 +1,7 @@ +package roomescape.domain.payment; + +public record PaymentFailure( + String code, + String message +) { +} diff --git a/src/main/java/roomescape/domain/payment/PaymentResponse.java b/src/main/java/roomescape/domain/payment/PaymentResponse.java new file mode 100644 index 000000000..157690a9f --- /dev/null +++ b/src/main/java/roomescape/domain/payment/PaymentResponse.java @@ -0,0 +1,27 @@ +package roomescape.domain.payment; + +import com.fasterxml.jackson.annotation.JsonFormat; +import roomescape.domain.reservation.PaymentInfo; + +import java.time.LocalDateTime; + +public record PaymentResponse( + String mId, + String paymentKey, + String orderId, + PaymentStatus status, + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX") + LocalDateTime requestedAt, + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX") + LocalDateTime approvedAt, + PaymentFailure failure, + Integer totalAmount +) { + public PaymentInfo toPaymentInfo() { + return new PaymentInfo(paymentKey, orderId, totalAmount); + } + + public boolean isNotDone() { + return status.isNotDone(); + } +} diff --git a/src/main/java/roomescape/domain/payment/PaymentStatus.java b/src/main/java/roomescape/domain/payment/PaymentStatus.java new file mode 100644 index 000000000..3722ef83e --- /dev/null +++ b/src/main/java/roomescape/domain/payment/PaymentStatus.java @@ -0,0 +1,17 @@ +package roomescape.domain.payment; + +public enum PaymentStatus { + READY, + IN_PROGRESS, + WAITING_FOR_DEPOSIT, + DONE, + CANCELED, + PARTIAL_CANCELED, + ABORTED, + EXPIRED, + ; + + public boolean isNotDone() { + return this != DONE; + } +} diff --git a/src/main/java/roomescape/domain/reservation/PaymentInfo.java b/src/main/java/roomescape/domain/reservation/PaymentInfo.java new file mode 100644 index 000000000..e2135e758 --- /dev/null +++ b/src/main/java/roomescape/domain/reservation/PaymentInfo.java @@ -0,0 +1,56 @@ +package roomescape.domain.reservation; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +import java.util.Objects; + +@Embeddable +public class PaymentInfo { + + @Column(unique = true) + private String paymentKey; + + @Column(unique = true) + private String orderId; + + private Integer amount; + + public PaymentInfo(String paymentKey, String orderId, Integer amount) { + this.paymentKey = paymentKey; + this.orderId = orderId; + this.amount = amount; + } + + protected PaymentInfo() { + } + + public String getPaymentKey() { + return paymentKey; + } + + public String getOrderId() { + return orderId; + } + + public Integer getAmount() { + return amount; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PaymentInfo that = (PaymentInfo) o; + return Objects.equals(paymentKey, that.paymentKey); + } + + @Override + public int hashCode() { + return Objects.hash(paymentKey, orderId); + } +} diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index 677f1c95c..3c391243a 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -30,16 +30,24 @@ public class Reservation { @OneToMany(mappedBy = "reservation", cascade = CascadeType.PERSIST, orphanRemoval = true) private List waitings = new ArrayList<>(); + @Embedded + private PaymentInfo paymentInfo; + private boolean isDeleted = false; + public Reservation(Member member, ReservationSlot slot, PaymentInfo paymentInfo) { + this(null, member, slot, paymentInfo); + } + public Reservation(Member member, ReservationSlot slot) { - this(null, member, slot); + this(null, member, slot, null); } - public Reservation(Long id, Member member, ReservationSlot slot) { + public Reservation(Long id, Member member, ReservationSlot slot, PaymentInfo paymentInfo) { this.id = id; this.member = member; this.slot = slot; + this.paymentInfo = paymentInfo; } protected Reservation() { @@ -88,6 +96,10 @@ public ReservationSlot getSlot() { return slot; } + public PaymentInfo getPaymentInfo() { + return paymentInfo; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/src/main/java/roomescape/infrastructure/PaymentClient.java b/src/main/java/roomescape/infrastructure/PaymentClient.java index c777d43c0..d524deb01 100644 --- a/src/main/java/roomescape/infrastructure/PaymentClient.java +++ b/src/main/java/roomescape/infrastructure/PaymentClient.java @@ -1,8 +1,9 @@ package roomescape.infrastructure; +import roomescape.domain.payment.PaymentResponse; import roomescape.service.dto.PaymentConfirmRequest; public interface PaymentClient { - void confirmPayment(PaymentConfirmRequest request); + PaymentResponse confirmPayment(PaymentConfirmRequest request); } diff --git a/src/main/java/roomescape/infrastructure/TossPaymentClient.java b/src/main/java/roomescape/infrastructure/TossPaymentClient.java index 5589c13ca..015cf7979 100644 --- a/src/main/java/roomescape/infrastructure/TossPaymentClient.java +++ b/src/main/java/roomescape/infrastructure/TossPaymentClient.java @@ -4,8 +4,10 @@ import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClient; +import roomescape.domain.payment.PaymentResponse; import roomescape.exception.PaymentException; import roomescape.service.dto.PaymentConfirmRequest; @@ -21,14 +23,16 @@ public TossPaymentClient(RestClient restClient, PaymentProperties paymentPropert } @Override - public void confirmPayment(PaymentConfirmRequest request) { + public PaymentResponse confirmPayment(PaymentConfirmRequest request) { try { - restClient.post() + ResponseEntity response = restClient.post() .uri(paymentProperties.url().paymentConfirm()) .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) .body(request) .retrieve() - .toBodilessEntity(); + .toEntity(PaymentResponse.class); + return response.getBody(); } catch (ResourceAccessException e) { logger.error("payment request body = {}" , request, e); throw new PaymentException(HttpStatus.INTERNAL_SERVER_ERROR, "결제 서버의 연결 가능 시간이 초과되었습니다."); diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index 312a6bdda..a495dd3fc 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -1,13 +1,16 @@ package roomescape.service; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import roomescape.domain.member.Member; import roomescape.domain.member.MemberRepository; +import roomescape.domain.payment.PaymentResponse; import roomescape.domain.reservation.*; import roomescape.domain.reservation.dto.ReservationReadOnly; import roomescape.domain.reservation.slot.ReservationSlot; import roomescape.exception.AuthorizationException; +import roomescape.exception.PaymentException; import roomescape.exception.RoomEscapeBusinessException; import roomescape.infrastructure.PaymentClient; import roomescape.service.dto.*; @@ -16,7 +19,6 @@ import java.util.Comparator; import java.util.List; import java.util.Optional; -import java.util.function.Supplier; import java.util.stream.Stream; @Service @@ -43,9 +45,19 @@ public ReservationService( } @Transactional - public ReservationResponse saveReservation(ReservationRequest reservationRequest) { - Runnable paymentStrategy = () -> {}; - return saveReservation(reservationRequest, paymentStrategy); + public ReservationResponse saveReservation(ReservationRequest reservationRequest) { // TODO: 중복 다시 제거 + Member member = findMemberById(reservationRequest.memberId()); + ReservationSlot slot = reservationSlotService.findSlot(reservationRequest.toSlotRequest()); + + Optional optionalReservation = reservationRepository.findBySlot(slot); + if (optionalReservation.isPresent()) { + Reservation reservation = optionalReservation.get(); + Waiting waiting = reservation.addWaiting(member); + waitingRepository.save(waiting); + return ReservationResponse.createByWaiting(waiting); + } + Reservation savedReservation = reservationRepository.save(new Reservation(member, slot)); + return ReservationResponse.createByReservation(savedReservation); } @Transactional @@ -53,32 +65,30 @@ public ReservationResponse saveReservation(ReservationPaymentRequest reservation ReservationRequest reservationRequest = reservationPaymentRequest.toReservationRequest(); PaymentConfirmRequest paymentConfirmRequest = reservationPaymentRequest.toPaymentRequest(); - Runnable paymentStrategy = () -> paymentClient.confirmPayment(paymentConfirmRequest); - return saveReservation(reservationRequest, paymentStrategy); - } - - private ReservationResponse saveReservation(ReservationRequest reservationRequest, Runnable paymentStrategy) { Member member = findMemberById(reservationRequest.memberId()); ReservationSlot slot = reservationSlotService.findSlot(reservationRequest.toSlotRequest()); - return trySave(member, slot, () -> { - paymentStrategy.run(); - Reservation savedReservation = reservationRepository.save(new Reservation(member, slot)); - return ReservationResponse.createByReservation(savedReservation); - }); + + Optional optionalReservation = reservationRepository.findBySlot(slot); + if (optionalReservation.isPresent()) { + Reservation reservation = optionalReservation.get(); + Waiting waiting = reservation.addWaiting(member); + waitingRepository.save(waiting); + return ReservationResponse.createByWaiting(waiting); + } + PaymentResponse paymentResponse = paymentClient.confirmPayment(paymentConfirmRequest); + if (paymentResponse.isNotDone()) { + throw new PaymentException(HttpStatus.INTERNAL_SERVER_ERROR, "결제를 실패하였습니다. 다시 시도해주세요."); + } + PaymentInfo paymentInfo = paymentResponse.toPaymentInfo(); + Reservation savedReservation = reservationRepository.save(new Reservation(member, slot, paymentInfo)); + return ReservationResponse.createByReservation(savedReservation); } @Transactional public ReservationResponse saveWaiting(WaitingSaveRequest waitingSaveRequest) { Member member = findMemberById(waitingSaveRequest.memberId()); ReservationSlot slot = reservationSlotService.findSlot(waitingSaveRequest.toSlotRequest()); - return trySave(member, slot, () -> { - throw new RoomEscapeBusinessException("해당 대기에 대한 예약이 존재하지 않습니다."); - }); - } - private ReservationResponse trySave(Member member, - ReservationSlot slot, - Supplier progressWhenReservationNotExist) { Optional optionalReservation = reservationRepository.findBySlot(slot); if (optionalReservation.isPresent()) { Reservation reservation = optionalReservation.get(); @@ -86,7 +96,7 @@ private ReservationResponse trySave(Member member, waitingRepository.save(waiting); return ReservationResponse.createByWaiting(waiting); } - return progressWhenReservationNotExist.get(); + throw new RoomEscapeBusinessException("해당 대기에 대한 예약이 존재하지 않습니다."); } @Transactional(readOnly = true) diff --git a/src/main/java/roomescape/service/dto/UserReservationResponse.java b/src/main/java/roomescape/service/dto/UserReservationResponse.java index 359ad1b9d..acc1a84f2 100644 --- a/src/main/java/roomescape/service/dto/UserReservationResponse.java +++ b/src/main/java/roomescape/service/dto/UserReservationResponse.java @@ -19,7 +19,9 @@ public record UserReservationResponse( @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm", timezone = "Asia/Seoul") LocalTime time, String status, - Long rank + Long rank, + String paymentKey, + Integer amount ) { public static Stream reservationsToResponseStream(List reservation) { return reservation.stream() @@ -34,7 +36,9 @@ private static UserReservationResponse createByReservation(Reservation reservati slot.getDate(), slot.getTime().getStartAt(), ReservationStatus.BOOKED.getValue(), - 0L + 0L, + reservation.getPaymentInfo().getPaymentKey(), + reservation.getPaymentInfo().getAmount() ); } @@ -52,7 +56,9 @@ private static UserReservationResponse createByWaiting(WaitingRank waitingRank) slot.getDate(), slot.getTime().getStartAt(), ReservationStatus.WAIT.getValue(), - waitingRank.rank() + waitingRank.rank(), + null, + null ); } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index c360d1ee8..b52ef5fad 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -11,7 +11,8 @@ VALUES ('이름1', '설명1', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f ('이름10', '설명10', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'), ('이름11', '설명11', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'), ('이름12', '설명12', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'), - ('이름13', '설명13', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'); + ('이름13', '설명13', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg') +; INSERT INTO reservation_time (start_at) VALUES ('09:00'), ('10:00'), @@ -19,33 +20,37 @@ VALUES ('09:00'), ('12:00'), ('13:00'), ('14:00'), - ('15:00'); + ('15:00') +; INSERT INTO member (name, email, password, role) VALUES ('어드민', 'admin@admin.com', '1234', 'ADMIN'), ('유저1', 'user1@user.com', '1234', 'USER'), ('유저2', 'user2@user.com', '1234', 'USER'), - ('유저3', 'user3@user.com', '1234', 'USER'); -INSERT INTO reservation (member_id, date, time_id, theme_id, is_deleted) -VALUES (1, '2024-05-04', 1, 1, false), - (1, '2024-05-04', 2, 1, false), - (1, '2024-05-05', 3, 1, false), - (2, '2024-05-05', 1, 2, false), - (2, '2024-05-05', 1, 3, false), - (2, '2024-05-09', 1, 2, false), - (2, '2024-05-05', 1, 4, false), - (3, '2024-05-06', 1, 2, false), - (3, '2024-05-07', 1, 7, false), - (3, '2024-05-08', 1, 8, false), - (3, '2024-05-09', 1, 9, false), - (3, '2024-05-10', 1, 10, false), - (3, '2024-05-29', 2, 2, false), - (1, '2024-05-30', 1, 1, false), - (2, '2024-05-30', 2, 2, false), - (2, '2024-05-30', 3, 2, false), - (2, '2024-05-30', 4, 2, false), - (2, '2024-05-30', 7, 2, false) + ('유저3', 'user3@user.com', '1234', 'USER') +; +INSERT INTO reservation (member_id, date, time_id, theme_id, payment_key, order_id, amount, is_deleted) +VALUES (1, '2024-06-04', 1, 1, 'paymentKey1', 'orderId1', 1000, false), + (1, '2024-06-04', 2, 1, 'paymentKey2', 'orderId2', 1000, false), + (1, '2024-06-05', 3, 1, 'paymentKey3', 'orderId3', 1000, false), + (2, '2024-06-05', 1, 2, 'paymentKey4', 'orderId4', 1000, false), + (2, '2024-06-05', 1, 3, 'paymentKey5', 'orderId5', 1000, false), + (2, '2024-06-09', 1, 2, 'paymentKey6', 'orderId6', 1000, false), + (2, '2024-06-05', 1, 4, 'paymentKey7', 'orderId7', 1000, false), + (3, '2024-06-06', 1, 2, 'paymentKey8', 'orderId8', 1000, false), + (3, '2024-06-07', 1, 7, 'paymentKey9', 'orderId9', 1000, false), + (3, '2024-06-08', 1, 8, 'paymentKey10', 'orderId10', 1000, false), + (3, '2024-06-09', 1, 9, 'paymentKey11', 'orderId11', 1000, false), + (3, '2024-06-10', 1, 10, 'paymentKey12', 'orderId12', 1000, false), + (3, '2024-06-29', 2, 2, 'paymentKey13', 'orderId13', 1000, false), + (1, '2024-06-30', 1, 1, 'paymentKey14', 'orderId14', 1000, false), + (2, '2024-06-30', 2, 2, 'paymentKey15', 'orderId15', 1000, false), + (2, '2024-06-30', 3, 2, 'paymentKey16', 'orderId16', 1000, false), + (2, '2024-06-30', 4, 2, 'paymentKey17', 'orderId17', 1000, false), + (2, '2024-06-30', 7, 2, 'paymentKey18', 'orderId18', 1000, false) ; INSERT INTO waiting (member_id, reservation_id, is_deleted) VALUES (3, 15, false), + (2, 15, false), (1, 15, false), - (1, 16, false); + (1, 16, false) +; diff --git a/src/main/resources/static/css/toss-style.css b/src/main/resources/static/css/toss-style.css index 2b311dd2d..9f7a6b1af 100644 --- a/src/main/resources/static/css/toss-style.css +++ b/src/main/resources/static/css/toss-style.css @@ -83,9 +83,10 @@ a { .button-group { margin-top: 32px; + margin-bottom: 32px; display: flex; - flex-direction: column; - justify-content: center; + flex-direction: row; + justify-content: space-between; gap: 16px; } diff --git a/src/main/resources/static/docs/index.html b/src/main/resources/static/docs/index.html new file mode 100644 index 000000000..dc7e7ed43 --- /dev/null +++ b/src/main/resources/static/docs/index.html @@ -0,0 +1,2603 @@ + + + + + + + +1️⃣ 회원 + + + + + +
+
+

1️⃣ 회원

+
+
+

조회

+
+

Request cookies

+ ++++ + + + + + + + + + + + + +
NameDescription

token

회원 인증 토큰 (어드민이어야 합니다)

+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

[]

Array

응답 배열

[].id

Number

[].name

String

이름

[].role

String

역할

+
+
+

HTTP request

+
+
+
GET /admin/members HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MSwiaWF0IjoxNzE4MjUzNzgxfQ.1VxqgzsmQKWmrR3xseszmVfUl7V8pC46FN3uC_TtU6M
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+[ {
+  "id" : 1,
+  "name" : "어드민",
+  "role" : "ADMIN"
+}, {
+  "id" : 2,
+  "name" : "유저1",
+  "role" : "USER"
+}, {
+  "id" : 3,
+  "name" : "유저2",
+  "role" : "USER"
+}, {
+  "id" : 4,
+  "name" : "유저3",
+  "role" : "USER"
+} ]
+
+
+
+
+
+

저장

+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

email

String

이메일

password

String

비밀번호

name

String

이름

+
+
+

HTTP request

+
+
+
POST /members HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+
+{
+  "name" : "ever",
+  "email" : "ever@email.com",
+  "password" : "password"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 201 Created
+Location: /members/6
+Content-Type: application/json
+
+{
+  "id" : 6,
+  "name" : "ever",
+  "role" : "USER"
+}
+
+
+
+
+
+

삭제

+
+

Request cookies

+ ++++ + + + + + + + + + + + + +
NameDescription

token

회원 인증 토큰 (어드민이어야 합니다)

+
+
+

HTTP request

+
+
+
DELETE /admin/members/5 HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MSwiaWF0IjoxNzE4MjUzNzgxfQ.1VxqgzsmQKWmrR3xseszmVfUl7V8pC46FN3uC_TtU6M
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 204 No Content
+
+
+
+
+
+
+
+

2️⃣ 인증

+
+
+

로그인

+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

email

String

이메일

password

String

패스워드

+
+
+

HTTP request

+
+
+
POST /login HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+
+{
+  "email" : "admin@admin.com",
+  "password" : "1234"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Set-Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MSwiaWF0IjoxNzE4MjUzNzgxfQ.1VxqgzsmQKWmrR3xseszmVfUl7V8pC46FN3uC_TtU6M; Path=/; HttpOnly
+
+
+
+
+
+

로그아웃

+
+

Request cookies

+ ++++ + + + + + + + + + + + + +
NameDescription

token

회원 인증 토큰

+
+
+

HTTP request

+
+
+
POST /logout HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/x-www-form-urlencoded; charset=ISO-8859-1
+Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MSwiaWF0IjoxNzE4MjUzNzgxfQ.1VxqgzsmQKWmrR3xseszmVfUl7V8pC46FN3uC_TtU6M
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Set-Cookie: token=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:10 GMT
+
+
+
+
+
+

로그인 여부 확인

+
+

Request cookies

+ ++++ + + + + + + + + + + + + +
NameDescription

token

회원 인증 토큰

+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

name

String

이름

role

String

역할

+
+
+

HTTP request

+
+
+
GET /login/check HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MSwiaWF0IjoxNzE4MjUzNzgxfQ.1VxqgzsmQKWmrR3xseszmVfUl7V8pC46FN3uC_TtU6M
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+{
+  "id" : 1,
+  "name" : "어드민",
+  "role" : "ADMIN"
+}
+
+
+
+
+
+
+
+

3️⃣ 예약 시간

+
+
+

조회

+
+

Request cookies

+ ++++ + + + + + + + + + + + + +
NameDescription

token

회원 인증 토큰

+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

[]

Array

응답 배열

[].id

Number

[].startAt

String

시간

+
+
+

HTTP request

+
+
+
GET /times HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MiwiaWF0IjoxNzE4MjUzNzgyfQ.waypmSNR_NAoBJSF9S6lhoYv9i6DR-wDlVaKMx6k0CM
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+[ {
+  "id" : 1,
+  "startAt" : "09:00"
+}, {
+  "id" : 2,
+  "startAt" : "10:00"
+}, {
+  "id" : 3,
+  "startAt" : "11:00"
+}, {
+  "id" : 4,
+  "startAt" : "12:00"
+}, {
+  "id" : 5,
+  "startAt" : "13:00"
+}, {
+  "id" : 6,
+  "startAt" : "14:00"
+}, {
+  "id" : 7,
+  "startAt" : "15:00"
+} ]
+
+
+
+
+
+

저장

+
+

Request cookies

+ ++++ + + + + + + + + + + + + +
NameDescription

token

회원 인증 토큰 (어드민이어야 합니다)

+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + +
PathTypeDescription

startAt

String

시간

+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

startAt

String

시간

+
+
+

HTTP request

+
+
+
POST /admin/times HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MiwiaWF0IjoxNzE4MjUzNzgyfQ.waypmSNR_NAoBJSF9S6lhoYv9i6DR-wDlVaKMx6k0CM
+
+{
+  "startAt" : "12:12"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 201 Created
+Location: /times/8
+Content-Type: application/json
+
+{
+  "id" : 8,
+  "startAt" : "12:12"
+}
+
+
+
+
+
+

삭제

+
+

Request cookies

+ ++++ + + + + + + + + + + + + +
NameDescription

token

회원 인증 토큰 (어드민이어야 합니다)

+
+
+

HTTP request

+
+
+
DELETE /admin/times/8 HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MiwiaWF0IjoxNzE4MjUzNzgyfQ.waypmSNR_NAoBJSF9S6lhoYv9i6DR-wDlVaKMx6k0CM
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 204 No Content
+
+
+
+
+
+
+
+

4️⃣ 테마

+
+
+

조회

+
+

Request cookies

+ ++++ + + + + + + + + + + + + +
NameDescription

token

회원 인증 토큰

+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

[]

Array

응답 배열

[].id

Number

[].name

String

이름

[].description

String

설명

[].thumbnail

String

썸네일

+
+
+

HTTP request

+
+
+
GET /themes HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MiwiaWF0IjoxNzE4MjUzNzgyfQ.waypmSNR_NAoBJSF9S6lhoYv9i6DR-wDlVaKMx6k0CM
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+[ {
+  "id" : 1,
+  "name" : "이름1",
+  "description" : "설명1",
+  "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+}, {
+  "id" : 2,
+  "name" : "이름2",
+  "description" : "설명2",
+  "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+}, {
+  "id" : 3,
+  "name" : "이름3",
+  "description" : "설명3",
+  "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+}, {
+  "id" : 4,
+  "name" : "이름4",
+  "description" : "설명4",
+  "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+}, {
+  "id" : 5,
+  "name" : "이름5",
+  "description" : "설명5",
+  "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+}, {
+  "id" : 6,
+  "name" : "이름6",
+  "description" : "설명6",
+  "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+}, {
+  "id" : 7,
+  "name" : "이름7",
+  "description" : "설명7",
+  "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+}, {
+  "id" : 8,
+  "name" : "이름8",
+  "description" : "설명8",
+  "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+}, {
+  "id" : 9,
+  "name" : "이름9",
+  "description" : "설명9",
+  "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+}, {
+  "id" : 10,
+  "name" : "이름10",
+  "description" : "설명10",
+  "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+}, {
+  "id" : 11,
+  "name" : "이름11",
+  "description" : "설명11",
+  "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+}, {
+  "id" : 12,
+  "name" : "이름12",
+  "description" : "설명12",
+  "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+}, {
+  "id" : 13,
+  "name" : "이름13",
+  "description" : "설명13",
+  "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+} ]
+
+
+
+
+
+

저장

+
+

Request cookies

+ ++++ + + + + + + + + + + + + +
NameDescription

token

회원 인증 토큰

+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

name

String

이름

description

String

설명

thumbnail

String

썸네일

+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

name

String

이름

description

String

설명

thumbnail

String

썸네일

+
+
+

HTTP request

+
+
+
POST /admin/themes HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MiwiaWF0IjoxNzE4MjUzNzgyfQ.waypmSNR_NAoBJSF9S6lhoYv9i6DR-wDlVaKMx6k0CM
+
+{
+  "thumbnail" : "썸네일",
+  "name" : "테마_테스트",
+  "description" : "설명_테스트"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 201 Created
+Location: /themes/14
+Content-Type: application/json
+
+{
+  "id" : 14,
+  "name" : "테마_테스트",
+  "description" : "설명_테스트",
+  "thumbnail" : "썸네일"
+}
+
+
+
+
+
+

삭제

+
+

Request cookies

+ ++++ + + + + + + + + + + + + +
NameDescription

token

회원 인증 토큰 (어드민이어야 합니다)

+
+
+

HTTP request

+
+
+
DELETE /admin/themes/14 HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MiwiaWF0IjoxNzE4MjUzNzgyfQ.waypmSNR_NAoBJSF9S6lhoYv9i6DR-wDlVaKMx6k0CM
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 204 No Content
+
+
+
+
+
+

인기 조회

+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

[]

Array

응답 배열

[].id

Number

[].name

String

이름

[].description

String

설명

[].thumbnail

String

썸네일

+
+
+

HTTP request

+
+
+
GET /themes/popular?limit=2&startDate=2000-01-01&endDate=9999-09-09 HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+[ {
+  "id" : 2,
+  "name" : "이름2",
+  "description" : "설명2",
+  "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+}, {
+  "id" : 1,
+  "name" : "이름1",
+  "description" : "설명1",
+  "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+} ]
+
+
+
+
+
+
+
+

5️⃣ 예약

+
+
+

조회

+
+

Request cookies

+ ++++ + + + + + + + + + + + + +
NameDescription

token

회원 인증 토큰 (어드민이어야 합니다)

+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

[]

Array

응답 배열

[].id

Number

[].date

String

추가 날짜

[].member

Object

회원

[].member.id

Number

회원 - 키

[].member.name

String

회원 - 이름

[].member.role

String

회원 - 역할

[].time

Object

시간

[].time.id

Number

시간 - 키

[].time.startAt

String

시간 - 시작 시간

[].theme

Object

테마

[].theme.id

Number

테마 - 키

[].theme.name

String

테마 - 이름

[].theme.description

String

테마 - 설명

[].theme.thumbnail

String

테마 - 썸네일

[].status

String

예약 혹은 대기 상태

+
+
+

HTTP request

+
+
+
GET /admin/reservations HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MiwiaWF0IjoxNzE4MjUzNzgyfQ.waypmSNR_NAoBJSF9S6lhoYv9i6DR-wDlVaKMx6k0CM
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+[ {
+  "id" : 1,
+  "member" : {
+    "id" : 1,
+    "name" : "어드민",
+    "role" : "ADMIN"
+  },
+  "date" : "2024-06-04",
+  "time" : {
+    "id" : 1,
+    "startAt" : "09:00"
+  },
+  "theme" : {
+    "id" : 1,
+    "name" : "이름1",
+    "description" : "설명1",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 2,
+  "member" : {
+    "id" : 1,
+    "name" : "어드민",
+    "role" : "ADMIN"
+  },
+  "date" : "2024-06-04",
+  "time" : {
+    "id" : 2,
+    "startAt" : "10:00"
+  },
+  "theme" : {
+    "id" : 1,
+    "name" : "이름1",
+    "description" : "설명1",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 4,
+  "member" : {
+    "id" : 2,
+    "name" : "유저1",
+    "role" : "USER"
+  },
+  "date" : "2024-06-05",
+  "time" : {
+    "id" : 1,
+    "startAt" : "09:00"
+  },
+  "theme" : {
+    "id" : 2,
+    "name" : "이름2",
+    "description" : "설명2",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 5,
+  "member" : {
+    "id" : 2,
+    "name" : "유저1",
+    "role" : "USER"
+  },
+  "date" : "2024-06-05",
+  "time" : {
+    "id" : 1,
+    "startAt" : "09:00"
+  },
+  "theme" : {
+    "id" : 3,
+    "name" : "이름3",
+    "description" : "설명3",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 7,
+  "member" : {
+    "id" : 2,
+    "name" : "유저1",
+    "role" : "USER"
+  },
+  "date" : "2024-06-05",
+  "time" : {
+    "id" : 1,
+    "startAt" : "09:00"
+  },
+  "theme" : {
+    "id" : 4,
+    "name" : "이름4",
+    "description" : "설명4",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 3,
+  "member" : {
+    "id" : 1,
+    "name" : "어드민",
+    "role" : "ADMIN"
+  },
+  "date" : "2024-06-05",
+  "time" : {
+    "id" : 3,
+    "startAt" : "11:00"
+  },
+  "theme" : {
+    "id" : 1,
+    "name" : "이름1",
+    "description" : "설명1",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 8,
+  "member" : {
+    "id" : 3,
+    "name" : "유저2",
+    "role" : "USER"
+  },
+  "date" : "2024-06-06",
+  "time" : {
+    "id" : 1,
+    "startAt" : "09:00"
+  },
+  "theme" : {
+    "id" : 2,
+    "name" : "이름2",
+    "description" : "설명2",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 9,
+  "member" : {
+    "id" : 3,
+    "name" : "유저2",
+    "role" : "USER"
+  },
+  "date" : "2024-06-07",
+  "time" : {
+    "id" : 1,
+    "startAt" : "09:00"
+  },
+  "theme" : {
+    "id" : 7,
+    "name" : "이름7",
+    "description" : "설명7",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 10,
+  "member" : {
+    "id" : 3,
+    "name" : "유저2",
+    "role" : "USER"
+  },
+  "date" : "2024-06-08",
+  "time" : {
+    "id" : 1,
+    "startAt" : "09:00"
+  },
+  "theme" : {
+    "id" : 8,
+    "name" : "이름8",
+    "description" : "설명8",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 6,
+  "member" : {
+    "id" : 2,
+    "name" : "유저1",
+    "role" : "USER"
+  },
+  "date" : "2024-06-09",
+  "time" : {
+    "id" : 1,
+    "startAt" : "09:00"
+  },
+  "theme" : {
+    "id" : 2,
+    "name" : "이름2",
+    "description" : "설명2",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 11,
+  "member" : {
+    "id" : 3,
+    "name" : "유저2",
+    "role" : "USER"
+  },
+  "date" : "2024-06-09",
+  "time" : {
+    "id" : 1,
+    "startAt" : "09:00"
+  },
+  "theme" : {
+    "id" : 9,
+    "name" : "이름9",
+    "description" : "설명9",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 12,
+  "member" : {
+    "id" : 3,
+    "name" : "유저2",
+    "role" : "USER"
+  },
+  "date" : "2024-06-10",
+  "time" : {
+    "id" : 1,
+    "startAt" : "09:00"
+  },
+  "theme" : {
+    "id" : 10,
+    "name" : "이름10",
+    "description" : "설명10",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 13,
+  "member" : {
+    "id" : 3,
+    "name" : "유저2",
+    "role" : "USER"
+  },
+  "date" : "2024-06-29",
+  "time" : {
+    "id" : 2,
+    "startAt" : "10:00"
+  },
+  "theme" : {
+    "id" : 2,
+    "name" : "이름2",
+    "description" : "설명2",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 14,
+  "member" : {
+    "id" : 1,
+    "name" : "어드민",
+    "role" : "ADMIN"
+  },
+  "date" : "2024-06-30",
+  "time" : {
+    "id" : 1,
+    "startAt" : "09:00"
+  },
+  "theme" : {
+    "id" : 1,
+    "name" : "이름1",
+    "description" : "설명1",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 15,
+  "member" : {
+    "id" : 2,
+    "name" : "유저1",
+    "role" : "USER"
+  },
+  "date" : "2024-06-30",
+  "time" : {
+    "id" : 2,
+    "startAt" : "10:00"
+  },
+  "theme" : {
+    "id" : 2,
+    "name" : "이름2",
+    "description" : "설명2",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 16,
+  "member" : {
+    "id" : 2,
+    "name" : "유저1",
+    "role" : "USER"
+  },
+  "date" : "2024-06-30",
+  "time" : {
+    "id" : 3,
+    "startAt" : "11:00"
+  },
+  "theme" : {
+    "id" : 2,
+    "name" : "이름2",
+    "description" : "설명2",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 17,
+  "member" : {
+    "id" : 2,
+    "name" : "유저1",
+    "role" : "USER"
+  },
+  "date" : "2024-06-30",
+  "time" : {
+    "id" : 4,
+    "startAt" : "12:00"
+  },
+  "theme" : {
+    "id" : 2,
+    "name" : "이름2",
+    "description" : "설명2",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 18,
+  "member" : {
+    "id" : 2,
+    "name" : "유저1",
+    "role" : "USER"
+  },
+  "date" : "2024-06-30",
+  "time" : {
+    "id" : 7,
+    "startAt" : "15:00"
+  },
+  "theme" : {
+    "id" : 2,
+    "name" : "이름2",
+    "description" : "설명2",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+} ]
+
+
+
+
+
+

저장

+
+

Request cookies

+ ++++ + + + + + + + + + + + + +
NameDescription

token

회원 인증 토큰

+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

date

String

날짜

memberId

Number

회원 키

timeId

Number

시간 키

themeId

Number

테마 키

+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

date

String

추가 날짜

member

Object

회원

member.id

Number

회원 - 키

member.name

String

회원 - 이름

member.role

String

회원 - 역할

time

Object

시간

time.id

Number

시간 - 키

time.startAt

String

시간 - 시작 시간

theme

Object

테마

theme.id

Number

테마 - 키

theme.name

String

테마 - 이름

theme.description

String

테마 - 설명

theme.thumbnail

String

테마 - 썸네일

status

String

예약 혹은 대기 상태

+
+
+

HTTP request

+
+
+
POST /admin/reservations HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MiwiaWF0IjoxNzE4MjUzNzgyfQ.waypmSNR_NAoBJSF9S6lhoYv9i6DR-wDlVaKMx6k0CM
+
+{
+  "themeId" : 1,
+  "memberId" : 1,
+  "date" : "9999-09-09",
+  "timeId" : 1
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 201 Created
+Location: /reservations/19
+Content-Type: application/json
+
+{
+  "id" : 19,
+  "member" : {
+    "id" : 1,
+    "name" : "어드민",
+    "role" : "ADMIN"
+  },
+  "date" : "9999-09-09",
+  "time" : {
+    "id" : 1,
+    "startAt" : "09:00"
+  },
+  "theme" : {
+    "id" : 1,
+    "name" : "이름1",
+    "description" : "설명1",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "BOOKED"
+}
+
+
+
+
+
+

삭제

+
+

Request cookies

+ ++++ + + + + + + + + + + + + +
NameDescription

token

회원 인증 토큰 (어드민이어야 합니다)

+
+
+

HTTP request

+
+
+
DELETE /admin/reservations/19 HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MiwiaWF0IjoxNzE4MjUzNzgyfQ.waypmSNR_NAoBJSF9S6lhoYv9i6DR-wDlVaKMx6k0CM
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 204 No Content
+
+
+
+
+
+

검색 (필터링)

+
+

Request cookies

+ ++++ + + + + + + + + + + + + +
NameDescription

token

회원 인증 토큰 (어드민이어야 합니다)

+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

[]

Array

응답 배열

[].id

Number

[].date

String

추가 날짜

[].member

Object

회원

[].member.id

Number

회원 - 키

[].member.name

String

회원 - 이름

[].member.role

String

회원 - 역할

[].time

Object

시간

[].time.id

Number

시간 - 키

[].time.startAt

String

시간 - 시작 시간

[].theme

Object

테마

[].theme.id

Number

테마 - 키

[].theme.name

String

테마 - 이름

[].theme.description

String

테마 - 설명

[].theme.thumbnail

String

테마 - 썸네일

[].status

String

예약 혹은 대기 상태

+
+
+

HTTP request

+
+
+
GET /admin/reservations?themeId=1&memberId=1&dateFrom=2000-01-01&dateTo=9999-09-09 HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MSwiaWF0IjoxNzE4MjUzNzgxfQ.1VxqgzsmQKWmrR3xseszmVfUl7V8pC46FN3uC_TtU6M
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+[ {
+  "id" : 1,
+  "member" : {
+    "id" : 1,
+    "name" : "어드민",
+    "role" : "ADMIN"
+  },
+  "date" : "2024-06-04",
+  "time" : {
+    "id" : 1,
+    "startAt" : "09:00"
+  },
+  "theme" : {
+    "id" : 1,
+    "name" : "이름1",
+    "description" : "설명1",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 2,
+  "member" : {
+    "id" : 1,
+    "name" : "어드민",
+    "role" : "ADMIN"
+  },
+  "date" : "2024-06-04",
+  "time" : {
+    "id" : 2,
+    "startAt" : "10:00"
+  },
+  "theme" : {
+    "id" : 1,
+    "name" : "이름1",
+    "description" : "설명1",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 3,
+  "member" : {
+    "id" : 1,
+    "name" : "어드민",
+    "role" : "ADMIN"
+  },
+  "date" : "2024-06-05",
+  "time" : {
+    "id" : 3,
+    "startAt" : "11:00"
+  },
+  "theme" : {
+    "id" : 1,
+    "name" : "이름1",
+    "description" : "설명1",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+}, {
+  "id" : 14,
+  "member" : {
+    "id" : 1,
+    "name" : "어드민",
+    "role" : "ADMIN"
+  },
+  "date" : "2024-06-30",
+  "time" : {
+    "id" : 1,
+    "startAt" : "09:00"
+  },
+  "theme" : {
+    "id" : 1,
+    "name" : "이름1",
+    "description" : "설명1",
+    "thumbnail" : "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg"
+  },
+  "status" : "예약 확정"
+} ]
+
+
+
+
+
+
+
+

6️⃣ 대기

+
+
+

조회

+
+

Request cookies

+ ++++ + + + + + + + + + + + + +
NameDescription

token

회원 인증 토큰 (어드민이어야 합니다)

+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

[]

Array

응답 배열

[].id

Number

[].name

String

회원 이름

[].theme

String

테마 이름

[].date

String

날짜

[].startAt

String

시간

+
+
+

HTTP request

+
+
+
GET /admin/reservations/waiting HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi7Ja065Oc66-8Iiwic3ViIjoiMSIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxODI1NzM4MSwiaWF0IjoxNzE4MjUzNzgxfQ.1VxqgzsmQKWmrR3xseszmVfUl7V8pC46FN3uC_TtU6M
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+[ {
+  "id" : 1,
+  "name" : "유저2",
+  "theme" : "이름2",
+  "date" : "2024-06-30",
+  "startAt" : "10:00:00"
+}, {
+  "id" : 2,
+  "name" : "어드민",
+  "theme" : "이름2",
+  "date" : "2024-06-30",
+  "startAt" : "10:00:00"
+}, {
+  "id" : 3,
+  "name" : "어드민",
+  "theme" : "이름2",
+  "date" : "2024-06-30",
+  "startAt" : "11:00:00"
+} ]
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/js/reservation-mine.js b/src/main/resources/static/js/reservation-mine.js index 844aeafd0..c6782a5d0 100644 --- a/src/main/resources/static/js/reservation-mine.js +++ b/src/main/resources/static/js/reservation-mine.js @@ -60,7 +60,14 @@ function render(data) { }; cancelCell.appendChild(cancelButton); } else { // 예약 완료 상태일 때 + /* + TODO: [미션4 - 2단계] 내 예약 목록 조회 시, + 예약 완료 상태일 때 결제 정보를 함께 보여주기 + 결제 정보 필드명은 자신의 response 에 맞게 변경하기 + */ row.insertCell(4).textContent = ''; + row.insertCell(5).textContent = item.paymentKey; + row.insertCell(6).textContent = item.amount; } }); } diff --git a/src/main/resources/static/js/user-reservation-payment.js b/src/main/resources/static/js/user-reservation-payment.js new file mode 100644 index 000000000..272a98a3c --- /dev/null +++ b/src/main/resources/static/js/user-reservation-payment.js @@ -0,0 +1,97 @@ +document.addEventListener('DOMContentLoaded', () => { + + // ------ 결제위젯 초기화 ------ + // @docs https://docs.tosspayments.com/reference/widget-sdk#sdk-설치-및-초기화 + // @docs https://docs.tosspayments.com/reference/widget-sdk#renderpaymentmethods선택자-결제-금액-옵션 + const paymentAmount = 1000; + const widgetClientKey = "test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm"; + + const paymentWidget = PaymentWidget(widgetClientKey, PaymentWidget.ANONYMOUS); + paymentWidget.renderPaymentMethods( + "#payment-method", + {value: paymentAmount}, + {variantKey: "DEFAULT"} + ); + + document.getElementById('reserve-button') + .addEventListener('click', onReservationButtonClickWithPaymentWidget); + + function onReservationButtonClickWithPaymentWidget(event) { + onReservationButtonClick(event, paymentWidget); + } +}); + +function onReservationButtonClick(event, paymentWidget) { + const params = new URLSearchParams(window.location.search); + const date = params.get('date'); + const themeId = params.get('themeId'); + const timeId = params.get('timeId'); + + const reservationData = { + date: date, + themeId: themeId, + timeId: timeId + }; + + const generateRandomString = () => + window.btoa(Math.random()).slice(0, 20); + + // 결제창 띄우기 + // TOSS 결제 위젯 Javascript SDK 연동 방식 중 'Promise로 처리하기'를 적용함 + // https://docs.tosspayments.com/reference/widget-sdk#promise%EB%A1%9C-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0 + const orderIdPrefix = "EVER"; + paymentWidget.requestPayment({ + orderId: orderIdPrefix + generateRandomString(), + orderName: "테스트 방탈출 예약 결제 1건", + amount: 1000, + }).then(function (data) { + console.debug(data); + fetchReservationPayment(data, reservationData); + }).catch(function (error) { + // TOSS 에러 처리: 에러 목록을 확인하세요 + // https://docs.tosspayments.com/reference/error-codes#failurl 로-전달되는-에러 + alert(error.code + " :" + error.message + "/ orderId : " + err.orderId); + }); +} + +async function fetchReservationPayment(paymentData, reservationData) { + /* + TODO: [1단계] + - 자신의 예약 API request에 맞게 reservationPaymentRequest 필드명 수정 + - 내 서버 URL에 맞게 reservationURL 변경 + - 예약 결제 실패 시, 사용자가 실패 사유를 알 수 있도록 alert 에서 에러 메시지 수정 + */ + const reservationPaymentRequest = { + date: reservationData.date, + themeId: reservationData.themeId, + timeId: reservationData.timeId, + paymentKey: paymentData.paymentKey, + orderId: paymentData.orderId, + amount: paymentData.amount, + paymentType: paymentData.paymentType, + } + + // 결제 요청 및 예약 저장 + const reservationURL = "/reservations"; + fetch(reservationURL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(reservationPaymentRequest), + }).then(response => { + if (!response.ok) { + return response.json().then(errorBody => { + console.error("예약 결제 실패 : " + JSON.stringify(errorBody)); + window.alert(errorBody.detail); + }); + } else { + response.json().then(successBody => { + console.log("예약 결제 성공 : " + JSON.stringify(successBody)); + window.location.href = "/reservation"; + }); + } + }).catch(error => { + console.error(error.message); + }); +} diff --git a/src/main/resources/static/js/user-reservation.js b/src/main/resources/static/js/user-reservation.js index 3fb293e4d..6fff36e16 100644 --- a/src/main/resources/static/js/user-reservation.js +++ b/src/main/resources/static/js/user-reservation.js @@ -1,301 +1,219 @@ const THEME_API_ENDPOINT = '/themes'; document.addEventListener('DOMContentLoaded', () => { - requestRead(THEME_API_ENDPOINT) - .then(renderTheme) - .catch(error => console.error('Error fetching times:', error)); - - flatpickr("#datepicker", { - inline: true, - onChange: function (selectedDates, dateStr, instance) { - if (dateStr === '') return; - checkDate(); - } - }); + requestRead(THEME_API_ENDPOINT) + .then(renderTheme) + .catch(error => console.error('Error fetching times:', error)); - // ------ 결제위젯 초기화 ------ - // @docs https://docs.tosspayments.com/reference/widget-sdk#sdk-설치-및-초기화 - // @docs https://docs.tosspayments.com/reference/widget-sdk#renderpaymentmethods선택자-결제-금액-옵션 - const paymentAmount = 1000; - const widgetClientKey = "test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm"; - const paymentWidget = PaymentWidget(widgetClientKey, PaymentWidget.ANONYMOUS); - paymentWidget.renderPaymentMethods( - "#payment-method", - {value: paymentAmount}, - {variantKey: "DEFAULT"} - ); + flatpickr("#datepicker", { + inline: true, + onChange: function (selectedDates, dateStr, instance) { + if (dateStr === '') return; + checkDate(); + } + }); - document.getElementById('theme-slots').addEventListener('click', event => { - if (event.target.classList.contains('theme-slot')) { - document.querySelectorAll('.theme-slot').forEach(slot => slot.classList.remove('active')); - event.target.classList.add('active'); - checkDateAndTheme(); - } - }); + document.getElementById('theme-slots').addEventListener('click', event => { + if (event.target.classList.contains('theme-slot')) { + document.querySelectorAll('.theme-slot').forEach(slot => slot.classList.remove('active')); + event.target.classList.add('active'); + checkDateAndTheme(); + } + }); - document.getElementById('time-slots').addEventListener('click', event => { - if (event.target.classList.contains('time-slot') && !event.target.classList.contains('disabled')) { - document.querySelectorAll('.time-slot').forEach(slot => slot.classList.remove('active')); - event.target.classList.add('active'); - checkDateAndThemeAndTime(); - } - }); + document.getElementById('time-slots').addEventListener('click', event => { + if (event.target.classList.contains('time-slot') && !event.target.classList.contains('disabled')) { + document.querySelectorAll('.time-slot').forEach(slot => slot.classList.remove('active')); + event.target.classList.add('active'); + checkDateAndThemeAndTime(); + } + }); - document.getElementById('reserve-button') - .addEventListener('click', onReservationButtonClickWithPaymentWidget); - document.getElementById('wait-button') - .addEventListener('click', onWaitButtonClick); - function onReservationButtonClickWithPaymentWidget(event) { - onReservationButtonClick(event, paymentWidget); - } + document.getElementById('reserve-button') + .addEventListener('click', onReservationButtonClick); + document.getElementById('wait-button') + .addEventListener('click', onWaitButtonClick); }); function renderTheme(themes) { - const themeSlots = document.getElementById('theme-slots'); - themeSlots.innerHTML = ''; - themes.forEach(theme => { - const name = theme.name; - const themeId = theme.id; - /* - TODO: [3단계] 사용자 예약 - 테마 목록 조회 API 호출 후 렌더링 - response 명세에 맞춰 createSlot 함수 호출 시 값 설정 - createSlot('theme', theme name, theme id) 형태로 호출 - */ - themeSlots.appendChild(createSlot('theme', name, themeId)); - }); + const themeSlots = document.getElementById('theme-slots'); + themeSlots.innerHTML = ''; + themes.forEach(theme => { + const name = theme.name; + const themeId = theme.id; + /* + TODO: [3단계] 사용자 예약 - 테마 목록 조회 API 호출 후 렌더링 + response 명세에 맞춰 createSlot 함수 호출 시 값 설정 + createSlot('theme', theme name, theme id) 형태로 호출 + */ + themeSlots.appendChild(createSlot('theme', name, themeId)); + }); } function createSlot(type, text, id, booked) { - const div = document.createElement('div'); - div.className = type + '-slot cursor-pointer bg-light border rounded p-3 mb-2'; - div.textContent = text; - div.setAttribute('data-' + type + '-id', id); - if (type === 'time') { - div.setAttribute('data-time-booked', booked); - } - return div; + const div = document.createElement('div'); + div.className = type + '-slot cursor-pointer bg-light border rounded p-3 mb-2'; + div.textContent = text; + div.setAttribute('data-' + type + '-id', id); + if (type === 'time') { + div.setAttribute('data-time-booked', booked); + } + return div; } function checkDate() { - const selectedDate = document.getElementById("datepicker").value; - if (selectedDate) { - const themeSection = document.getElementById("theme-section"); - if (themeSection.classList.contains("disabled")) { - themeSection.classList.remove("disabled"); + const selectedDate = document.getElementById("datepicker").value; + if (selectedDate) { + const themeSection = document.getElementById("theme-section"); + if (themeSection.classList.contains("disabled")) { + themeSection.classList.remove("disabled"); + } + const timeSlots = document.getElementById('time-slots'); + timeSlots.innerHTML = ''; + + requestRead(THEME_API_ENDPOINT) + .then(renderTheme) + .catch(error => console.error('Error fetching times:', error)); } - const timeSlots = document.getElementById('time-slots'); - timeSlots.innerHTML = ''; - - requestRead(THEME_API_ENDPOINT) - .then(renderTheme) - .catch(error => console.error('Error fetching times:', error)); - } } function checkDateAndTheme() { - const selectedDate = document.getElementById("datepicker").value; - const selectedThemeElement = document.querySelector('.theme-slot.active'); - if (selectedDate && selectedThemeElement) { - const selectedThemeId = selectedThemeElement.getAttribute('data-theme-id'); - fetchAvailableTimes(selectedDate, selectedThemeId); - } + const selectedDate = document.getElementById("datepicker").value; + const selectedThemeElement = document.querySelector('.theme-slot.active'); + if (selectedDate && selectedThemeElement) { + const selectedThemeId = selectedThemeElement.getAttribute('data-theme-id'); + fetchAvailableTimes(selectedDate, selectedThemeId); + } } function fetchAvailableTimes(date, themeId) { - /* - TODO: [3단계] 사용자 예약 - 예약 가능 시간 조회 API 호출 - 요청 포맷에 맞게 설정 - */ - fetch(`/times/booked?date=${date}&themeId=${themeId}`, { // 예약 가능 시간 조회 API endpoint - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }).then(response => { - if (response.status === 200) return response.json(); - throw new Error('Read failed'); - }).then(renderAvailableTimes) - .catch(error => console.error("Error fetching available times:", error)); + /* + TODO: [3단계] 사용자 예약 - 예약 가능 시간 조회 API 호출 + 요청 포맷에 맞게 설정 + */ + fetch(`/times/booked?date=${date}&themeId=${themeId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }).then(response => { + if (response.status === 200) return response.json(); + throw new Error('Read failed'); + }).then(renderAvailableTimes) + .catch(error => console.error("Error fetching available times:", error)); } function renderAvailableTimes(times) { - const timeSection = document.getElementById("time-section"); - if (timeSection.classList.contains("disabled")) { - timeSection.classList.remove("disabled"); - } - - const timeSlots = document.getElementById('time-slots'); - timeSlots.innerHTML = ''; - if (times.length === 0) { - timeSlots.innerHTML = '
선택할 수 있는 시간이 없습니다.
'; - return; - } - times.forEach(time => { - /* - TODO: [3단계] 사용자 예약 - 예약 가능 시간 조회 API 호출 후 렌더링 - response 명세에 맞춰 createSlot 함수 호출 시 값 설정 - */ - const startAt = time.startAt; - const timeId = time.id; - const alreadyBooked = time.alreadyBooked; + const timeSection = document.getElementById("time-section"); + if (timeSection.classList.contains("disabled")) { + timeSection.classList.remove("disabled"); + } - const div = createSlot('time', startAt, timeId, alreadyBooked); // createSlot('time', 시작 시간, time id, 예약 여부) - timeSlots.appendChild(div); - }); + const timeSlots = document.getElementById('time-slots'); + timeSlots.innerHTML = ''; + if (times.length === 0) { + timeSlots.innerHTML = '
선택할 수 있는 시간이 없습니다.
'; + return; + } + times.forEach(time => { + /* + TODO: [3단계] 사용자 예약 - 예약 가능 시간 조회 API 호출 후 렌더링 + response 명세에 맞춰 createSlot 함수 호출 시 값 설정 + */ + const startAt = time.startAt; + const timeId = time.id; + const alreadyBooked = time.alreadyBooked; + + const div = createSlot('time', startAt, timeId, alreadyBooked); // createSlot('time', 시작 시간, time id, 예약 여부) + timeSlots.appendChild(div); + }); } function checkDateAndThemeAndTime() { - const selectedDate = document.getElementById("datepicker").value; - const selectedThemeElement = document.querySelector('.theme-slot.active'); - const selectedTimeElement = document.querySelector('.time-slot.active'); - const reserveButton = document.getElementById("reserve-button"); - const waitButton = document.getElementById("wait-button"); - - if (selectedDate && selectedThemeElement && selectedTimeElement) { - if (selectedTimeElement.getAttribute('data-time-booked') === 'true') { - // 선택된 시간이 이미 예약된 경우 - reserveButton.classList.add("disabled"); - waitButton.classList.remove("disabled"); // 예약 대기 버튼 활성화 + const selectedDate = document.getElementById("datepicker").value; + const selectedThemeElement = document.querySelector('.theme-slot.active'); + const selectedTimeElement = document.querySelector('.time-slot.active'); + const reserveButton = document.getElementById("reserve-button"); + const waitButton = document.getElementById("wait-button"); + + if (selectedDate && selectedThemeElement && selectedTimeElement) { + if (selectedTimeElement.getAttribute('data-time-booked') === 'true') { + // 선택된 시간이 이미 예약된 경우 + reserveButton.classList.add("disabled"); + waitButton.classList.remove("disabled"); // 예약 대기 버튼 활성화 + } else { + // 선택된 시간이 예약 가능한 경우 + reserveButton.classList.remove("disabled"); + waitButton.classList.add("disabled"); // 예약 대기 버튼 비활성화 + } } else { - // 선택된 시간이 예약 가능한 경우 - reserveButton.classList.remove("disabled"); - waitButton.classList.add("disabled"); // 예약 대기 버튼 비활성화 + // 날짜, 테마, 시간 중 하나라도 선택되지 않은 경우 + reserveButton.classList.add("disabled"); + waitButton.classList.add("disabled"); } - } else { - // 날짜, 테마, 시간 중 하나라도 선택되지 않은 경우 - reserveButton.classList.add("disabled"); - waitButton.classList.add("disabled"); - } -} - -function onReservationButtonClick(event, paymentWidget) { - const selectedDate = document.getElementById("datepicker").value; - const selectedThemeId = document.querySelector('.theme-slot.active')?.getAttribute('data-theme-id'); - const selectedTimeId = document.querySelector('.time-slot.active')?.getAttribute('data-time-id'); - - if (selectedDate && selectedThemeId && selectedTimeId) { - - /* - TODO: [3단계] 사용자 예약 - 예약 요청 API 호출 - [5단계] 예약 생성 기능 변경 - 사용자 - request 명세에 맞게 설정 - */ - const reservationData = { - date: selectedDate, - themeId: selectedThemeId, - timeId: selectedTimeId - }; - - const generateRandomString = () => - window.btoa(Math.random()).slice(0, 20); - /* - TODO: [1단계] - - orderIdPrefix 를 자신만의 prefix로 변경 - */ - // TOSS 결제 위젯 Javascript SDK 연동 방식 중 'Promise로 처리하기'를 적용함 - // https://docs.tosspayments.com/reference/widget-sdk#promise%EB%A1%9C-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0 - const orderIdPrefix = "EVER"; - paymentWidget.requestPayment({ - orderId: orderIdPrefix + generateRandomString(), - orderName: "테스트 방탈출 예약 결제 1건", - amount: 1000, - }).then(function (data) { - console.debug(data); - fetchReservationPayment(data, reservationData); - }).catch(function (error) { - // TOSS 에러 처리: 에러 목록을 확인하세요 - // https://docs.tosspayments.com/reference/error-codes#failurl 로-전달되는-에러 - alert(error.code + " :" + error.message + "/ orderId : " + err.orderId); - }); - } else { - alert("Please select a date, theme, and time before making a reservation."); - } } -async function fetchReservationPayment(paymentData, reservationData) { - /* - TODO: [1단계] - - 자신의 예약 API request에 맞게 reservationPaymentRequest 필드명 수정 - - 내 서버 URL에 맞게 reservationURL 변경 - - 예약 결제 실패 시, 사용자가 실패 사유를 알 수 있도록 alert 에서 에러 메시지 수정 - */ - const reservationPaymentRequest = { - date: reservationData.date, - themeId: reservationData.themeId, - timeId: reservationData.timeId, - paymentKey: paymentData.paymentKey, - orderId: paymentData.orderId, - amount: paymentData.amount, - paymentType: paymentData.paymentType, - } - - const reservationURL = "/reservations"; - fetch(reservationURL, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(reservationPaymentRequest), - }).then(response => { - if (!response.ok) { - return response.json().then(errorBody => { - console.error("예약 결제 실패 : " + JSON.stringify(errorBody)); - window.alert(errorBody.detail); - }); +function onReservationButtonClick(event) { + const selectedDate = document.getElementById("datepicker").value; + const selectedThemeId = document.querySelector('.theme-slot.active')?.getAttribute('data-theme-id'); + const selectedTimeId = document.querySelector('.time-slot.active')?.getAttribute('data-time-id'); + + if (selectedDate && selectedThemeId && selectedTimeId) { + const reservationData = { + date: selectedDate, + themeId: selectedThemeId, + timeId: selectedTimeId + }; + const queryString = new URLSearchParams(reservationData).toString(); + window.location.href = `/reservation/payment?${queryString}`; } else { - response.json().then(successBody => { - console.log("예약 결제 성공 : " + JSON.stringify(successBody)); - window.location.reload(); - }); + alert("Please select a date, theme, and time before making a reservation."); } - }).catch(error => { - console.error(error.message); - }); } function onWaitButtonClick() { - const selectedDate = document.getElementById("datepicker").value; - const selectedThemeId = document.querySelector('.theme-slot.active')?.getAttribute('data-theme-id'); - const selectedTimeId = document.querySelector('.time-slot.active')?.getAttribute('data-time-id'); - - if (selectedDate && selectedThemeId && selectedTimeId) { - const reservationData = { - date: selectedDate, - themeId: selectedThemeId, - timeId: selectedTimeId - }; - - /* - TODO: [3단계] 예약 대기 생성 요청 API 호출 - */ - fetch('/reservations/waiting', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(reservationData) - }) - .then(response => { - if (!response.ok) throw new Error('Reservation waiting failed'); - return response.json(); - }) - .then(data => { - alert('Reservation waiting successful!'); - window.location.href = "/"; + const selectedDate = document.getElementById("datepicker").value; + const selectedThemeId = document.querySelector('.theme-slot.active')?.getAttribute('data-theme-id'); + const selectedTimeId = document.querySelector('.time-slot.active')?.getAttribute('data-time-id'); + + if (selectedDate && selectedThemeId && selectedTimeId) { + const reservationData = { + date: selectedDate, + themeId: selectedThemeId, + timeId: selectedTimeId + }; + + /* + TODO: [3단계] 예약 대기 생성 요청 API 호출 + */ + fetch('/reservations/waiting', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(reservationData) }) - .catch(error => { - alert("An error occurred while making the reservation waiting."); - console.error(error); - }); - } else { - alert("Please select a date, theme, and time before making a reservation waiting."); - } + .then(response => { + if (!response.ok) throw new Error('Reservation waiting failed'); + return response.json(); + }) + .then(data => { + alert('Reservation waiting successful!'); + window.location.href = "/"; + }) + .catch(error => { + alert("An error occurred while making the reservation waiting."); + console.error(error); + }); + } else { + alert("Please select a date, theme, and time before making a reservation waiting."); + } } function requestRead(endpoint) { - return fetch(endpoint) - .then(response => { - if (response.status === 200) return response.json(); - throw new Error('Read failed'); - }); + return fetch(endpoint) + .then(response => { + if (response.status === 200) return response.json(); + throw new Error('Read failed'); + }); } diff --git a/src/main/resources/templates/reservation-mine.html b/src/main/resources/templates/reservation-mine.html index 233bfde27..5808fc44c 100644 --- a/src/main/resources/templates/reservation-mine.html +++ b/src/main/resources/templates/reservation-mine.html @@ -53,6 +53,9 @@

내 예약

날짜 시간 상태 + 대기 취소 + paymentKey + 결제금액 diff --git a/src/main/resources/templates/reservation-payment.html b/src/main/resources/templates/reservation-payment.html new file mode 100644 index 000000000..c25d2820f --- /dev/null +++ b/src/main/resources/templates/reservation-payment.html @@ -0,0 +1,66 @@ + + + + + + 방탈출 예약 페이지 + + + + + + + + + + + + + + +
+
+
+
+
+ +
+
+
+ + + + + diff --git a/src/main/resources/templates/reservation.html b/src/main/resources/templates/reservation.html index a8b90bd99..3ee6c1623 100644 --- a/src/main/resources/templates/reservation.html +++ b/src/main/resources/templates/reservation.html @@ -79,33 +79,13 @@

시간 선택

- - - - - - -
- -
- - -
-
-
-
-
- -
+ +
- diff --git a/src/test/java/roomescape/IntegrationTestSupport.java b/src/test/java/roomescape/IntegrationTestSupport.java index a62689e50..1772506ce 100644 --- a/src/test/java/roomescape/IntegrationTestSupport.java +++ b/src/test/java/roomescape/IntegrationTestSupport.java @@ -2,14 +2,20 @@ import io.restassured.RestAssured; import jakarta.annotation.PostConstruct; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.MediaType; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringExtension; import roomescape.controller.dto.LoginRequest; import roomescape.infrastructure.PaymentClient; +@Sql("/init.sql") +@ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) public abstract class IntegrationTestSupport { diff --git a/src/test/java/roomescape/controller/AdminReservationControllerTest.java b/src/test/java/roomescape/controller/AdminReservationControllerTest.java index a59cbe415..92b158c60 100644 --- a/src/test/java/roomescape/controller/AdminReservationControllerTest.java +++ b/src/test/java/roomescape/controller/AdminReservationControllerTest.java @@ -1,38 +1,88 @@ package roomescape.controller; import io.restassured.RestAssured; +import io.restassured.http.ContentType; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import roomescape.IntegrationTestSupport; +import org.springframework.restdocs.cookies.RequestCookiesSnippet; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; +import org.springframework.restdocs.request.QueryParametersSnippet; +import roomescape.controller.config.ControllerTestSupport; import java.util.Map; import static org.hamcrest.Matchers.is; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; -class AdminReservationControllerTest extends IntegrationTestSupport { +class AdminReservationControllerTest extends ControllerTestSupport { @DisplayName("예약 내역을 필터링하여 조회한다.") @Test void findReservationByFilter() { - Map params = Map.of("themeId", "1", + RequestCookiesSnippet requestCookies = requestCookies( + cookieWithName("token").description("회원 인증 토큰 (어드민이어야 합니다)")); + QueryParametersSnippet requestParams = queryParameters( + parameterWithName("themeId").description("테마 키"), + parameterWithName("memberId").description("회원 키"), + parameterWithName("dateFrom").description("시작 날짜"), + parameterWithName("dateTo").description("끝 날짜")); + ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("[]").type(JsonFieldType.ARRAY).description("응답 배열"), + fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("키"), + fieldWithPath("[].date").type(JsonFieldType.STRING).description("추가 날짜"), + fieldWithPath("[].member").type(JsonFieldType.OBJECT).description("회원"), + fieldWithPath("[].member.id").type(JsonFieldType.NUMBER).description("회원 - 키"), + fieldWithPath("[].member.name").type(JsonFieldType.STRING).description("회원 - 이름"), + fieldWithPath("[].member.role").type(JsonFieldType.STRING).description("회원 - 역할"), + fieldWithPath("[].time").type(JsonFieldType.OBJECT).description("시간"), + fieldWithPath("[].time.id").type(JsonFieldType.NUMBER).description("시간 - 키"), + fieldWithPath("[].time.startAt").type(JsonFieldType.STRING).description("시간 - 시작 시간"), + fieldWithPath("[].theme").type(JsonFieldType.OBJECT).description("테마"), + fieldWithPath("[].theme.id").type(JsonFieldType.NUMBER).description("테마 - 키"), + fieldWithPath("[].theme.name").type(JsonFieldType.STRING).description("테마 - 이름"), + fieldWithPath("[].theme.description").type(JsonFieldType.STRING).description("테마 - 설명"), + fieldWithPath("[].theme.thumbnail").type(JsonFieldType.STRING).description("테마 - 썸네일"), + fieldWithPath("[].status").type(JsonFieldType.STRING).description("예약 혹은 대기 상태")); + + Map params = Map.of( + "themeId", "1", "memberId", "1", - "dateFrom", "2024-05-04", - "dateTo", "2024-05-04" + "dateFrom", "2000-01-01", + "dateTo", "9999-09-09" ); - RestAssured.given().log().all() + RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(requestCookies, requestParams, responseFields)) + .accept(ContentType.JSON) .cookies("token", ADMIN_TOKEN) .queryParams(params) .when().get("/admin/reservations") .then().log().all() .statusCode(200) - .body("size()", is(2)); + .body("size()", is(4)); } @DisplayName("예약 대기 목록을 조회한다.") @Test void findAllWaiting() { - RestAssured.given().log().all() + RequestCookiesSnippet requestCookies = requestCookies( + cookieWithName("token").description("회원 인증 토큰 (어드민이어야 합니다)")); + ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("[]").type(JsonFieldType.ARRAY).description("응답 배열"), + fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("키"), + fieldWithPath("[].name").type(JsonFieldType.STRING).description("회원 이름"), + fieldWithPath("[].theme").type(JsonFieldType.STRING).description("테마 이름"), + fieldWithPath("[].date").type(JsonFieldType.STRING).description("날짜"), + fieldWithPath("[].startAt").type(JsonFieldType.STRING).description("시간")); + RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(requestCookies, responseFields)) + .accept(ContentType.JSON) .cookies("token", ADMIN_TOKEN) .when().get("/admin/reservations/waiting") .then().log().all() diff --git a/src/test/java/roomescape/controller/LoginControllerTest.java b/src/test/java/roomescape/controller/LoginControllerTest.java index b451a344a..46d1bcbf9 100644 --- a/src/test/java/roomescape/controller/LoginControllerTest.java +++ b/src/test/java/roomescape/controller/LoginControllerTest.java @@ -1,12 +1,19 @@ package roomescape.controller; import io.restassured.RestAssured; +import io.restassured.http.ContentType; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import roomescape.IntegrationTestSupport; +import org.springframework.restdocs.cookies.RequestCookiesSnippet; +import org.springframework.restdocs.cookies.ResponseCookiesSnippet; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.RequestFieldsSnippet; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; +import roomescape.controller.config.ControllerTestSupport; import roomescape.controller.dto.LoginRequest; import roomescape.service.dto.MemberResponse; @@ -14,11 +21,67 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.springframework.restdocs.cookies.CookieDocumentation.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; -class LoginControllerTest extends IntegrationTestSupport { +class LoginControllerTest extends ControllerTestSupport { String accessToken; + @Test + @DisplayName("로그인") + void login() { + RequestFieldsSnippet requestFields = requestFields( + fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"), + fieldWithPath("password").type(JsonFieldType.STRING).description("패스워드")); + ResponseCookiesSnippet responseCookies = responseCookies( + cookieWithName("token").description("회원 인증 토큰")); + RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(requestFields, responseCookies)) + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body(new LoginRequest(ADMIN_EMAIL, ADMIN_PASSWORD)) + .when().post("/login") + .then().log().all() + .statusCode(200); + } + + @Test + @DisplayName("로그아웃") + void logout() { + RequestCookiesSnippet requestCookies = requestCookies( + cookieWithName("token").description("회원 인증 토큰")); + String cookie = RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(requestCookies)) + .accept(ContentType.JSON) + .cookie("token", ADMIN_TOKEN) + .when().post("/logout") + .then().log().all() + .statusCode(200).extract().cookie("token"); + + assertThat(cookie).isEmpty(); + } + + @Test + @DisplayName("로그인 확인") + void loginCheck() { + RequestCookiesSnippet requestCookies = requestCookies( + cookieWithName("token").description("회원 인증 토큰")); + ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("키"), + fieldWithPath("name").type(JsonFieldType.STRING).description("이름"), + fieldWithPath("role").type(JsonFieldType.STRING).description("역할")); + MemberResponse member = RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(requestCookies, responseFields)) + .accept(ContentType.JSON) + .cookie("token", ADMIN_TOKEN) + .when().get("/login/check") + .then().log().all() + .statusCode(200).extract().as(MemberResponse.class); + + assertThat(member.name()).isEqualTo(ADMIN_NAME); + } + @DisplayName("토큰으로 로그인 인증한다.") @TestFactory Stream dynamicTestsFromCollection() { diff --git a/src/test/java/roomescape/controller/MemberControllerTest.java b/src/test/java/roomescape/controller/MemberControllerTest.java index 46ab77aeb..eb5708624 100644 --- a/src/test/java/roomescape/controller/MemberControllerTest.java +++ b/src/test/java/roomescape/controller/MemberControllerTest.java @@ -4,25 +4,100 @@ import io.restassured.http.ContentType; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import org.springframework.http.MediaType; -import roomescape.IntegrationTestSupport; +import org.springframework.restdocs.cookies.RequestCookiesSnippet; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.RequestFieldsSnippet; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; +import roomescape.controller.config.ControllerTestSupport; import java.util.Map; import java.util.stream.Stream; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; -class MemberControllerTest extends IntegrationTestSupport { +class MemberControllerTest extends ControllerTestSupport { - public static final String TEST_EMAIL = "test@test.com"; - public static final String TEST_PASSWORD = "1234"; - public static final String TEST_NAME = "테스트"; + private static final String TEST_EMAIL = "test@test.com"; + private static final String TEST_PASSWORD = "1234"; + private static final String TEST_NAME = "테스트"; String createdId; int memberSize; + @Test + @DisplayName("회원 목록 조회") + void showMember() { + RequestCookiesSnippet requestCookies = requestCookies( + cookieWithName("token").description("회원 인증 토큰 (어드민이어야 합니다)")); + ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("[]").type(JsonFieldType.ARRAY).description("응답 배열"), + fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("키"), + fieldWithPath("[].name").type(JsonFieldType.STRING).description("이름"), + fieldWithPath("[].role").type(JsonFieldType.STRING).description("역할")); + RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(requestCookies, responseFields)) + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .cookie("token", ADMIN_TOKEN) + .when().get("/admin/members") + .then().log().all() + .statusCode(200); + } + + @Test + @DisplayName("회원 추가") + void saveMember() { + RequestFieldsSnippet requestFields = requestFields( + fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"), + fieldWithPath("password").type(JsonFieldType.STRING).description("비밀번호"), + fieldWithPath("name").type(JsonFieldType.STRING).description("이름")); + Map params = Map.of( + "email", "ever@email.com", + "password", "password", + "name", "ever"); + RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(requestFields)) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(ContentType.JSON) + .body(params) + .when().post("/members") + .then().log().all() + .statusCode(201); + } + + @Test + @DisplayName("회원 삭제") + void deleteMember() { + RequestCookiesSnippet requestCookies = requestCookies( + cookieWithName("token").description("회원 인증 토큰 (어드민이어야 합니다)")); + Map params = Map.of( + "email", "ever2@email.com", + "password", "password", + "name", "ever2"); + String createdId = RestAssured.given().log().all() + .body(params) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().post("/members") + .then().log().all() + .statusCode(201).extract().header("location").split("/")[2]; + + RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(requestCookies)) + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .cookie("token", ADMIN_TOKEN) + .when().delete("/admin/members/" + createdId) + .then().log().all() + .statusCode(204); + } + @DisplayName("회원 CRUD") @TestFactory Stream dynamicUserTestsFromCollection() { diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java index d52fbb3b2..11428c538 100644 --- a/src/test/java/roomescape/controller/ReservationControllerTest.java +++ b/src/test/java/roomescape/controller/ReservationControllerTest.java @@ -4,11 +4,20 @@ import io.restassured.http.ContentType; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; -import roomescape.IntegrationTestSupport; +import org.springframework.restdocs.cookies.RequestCookiesSnippet; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.RequestFieldsSnippet; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; +import roomescape.controller.config.ControllerTestSupport; +import roomescape.domain.payment.PaymentResponse; +import roomescape.domain.payment.PaymentStatus; +import roomescape.service.dto.PaymentConfirmRequest; import roomescape.service.dto.ReservationStatus; import roomescape.service.dto.UserReservationResponse; +import java.time.LocalDateTime; import java.time.LocalTime; import java.util.Arrays; import java.util.Map; @@ -18,15 +27,125 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.mockito.Mockito.doReturn; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; - -class ReservationControllerTest extends IntegrationTestSupport { +class ReservationControllerTest extends ControllerTestSupport { String userReservationId; String adminReservationId; int adminReservationSize; int userReservationSize; + @Test + @DisplayName("예약 목록 조회") + void showReservation() { + RequestCookiesSnippet requestCookies = requestCookies( + cookieWithName("token").description("회원 인증 토큰 (어드민이어야 합니다)")); + ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("[]").type(JsonFieldType.ARRAY).description("응답 배열"), + fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("키"), + fieldWithPath("[].date").type(JsonFieldType.STRING).description("추가 날짜"), + fieldWithPath("[].member").type(JsonFieldType.OBJECT).description("회원"), + fieldWithPath("[].member.id").type(JsonFieldType.NUMBER).description("회원 - 키"), + fieldWithPath("[].member.name").type(JsonFieldType.STRING).description("회원 - 이름"), + fieldWithPath("[].member.role").type(JsonFieldType.STRING).description("회원 - 역할"), + fieldWithPath("[].time").type(JsonFieldType.OBJECT).description("시간"), + fieldWithPath("[].time.id").type(JsonFieldType.NUMBER).description("시간 - 키"), + fieldWithPath("[].time.startAt").type(JsonFieldType.STRING).description("시간 - 시작 시간"), + fieldWithPath("[].theme").type(JsonFieldType.OBJECT).description("테마"), + fieldWithPath("[].theme.id").type(JsonFieldType.NUMBER).description("테마 - 키"), + fieldWithPath("[].theme.name").type(JsonFieldType.STRING).description("테마 - 이름"), + fieldWithPath("[].theme.description").type(JsonFieldType.STRING).description("테마 - 설명"), + fieldWithPath("[].theme.thumbnail").type(JsonFieldType.STRING).description("테마 - 썸네일"), + fieldWithPath("[].status").type(JsonFieldType.STRING).description("예약 혹은 대기 상태")); + RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(requestCookies, responseFields)) + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .cookie("token", ADMIN_TOKEN) + .when().get("/admin/reservations") + .then().log().all() + .statusCode(200); + } + + @Test + @DisplayName("예약 추가") + void saveReservation() { + RequestCookiesSnippet requestCookies = requestCookies( + cookieWithName("token").description("회원 인증 토큰")); + RequestFieldsSnippet requestFields = requestFields( + fieldWithPath("date").type(JsonFieldType.STRING).description("날짜"), + fieldWithPath("memberId").type(JsonFieldType.NUMBER).description("회원 키"), + fieldWithPath("timeId").type(JsonFieldType.NUMBER).description("시간 키"), + fieldWithPath("themeId").type(JsonFieldType.NUMBER).description("테마 키")); + ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("키"), + fieldWithPath("date").type(JsonFieldType.STRING).description("추가 날짜"), + fieldWithPath("member").type(JsonFieldType.OBJECT).description("회원"), + fieldWithPath("member.id").type(JsonFieldType.NUMBER).description("회원 - 키"), + fieldWithPath("member.name").type(JsonFieldType.STRING).description("회원 - 이름"), + fieldWithPath("member.role").type(JsonFieldType.STRING).description("회원 - 역할"), + fieldWithPath("time").type(JsonFieldType.OBJECT).description("시간"), + fieldWithPath("time.id").type(JsonFieldType.NUMBER).description("시간 - 키"), + fieldWithPath("time.startAt").type(JsonFieldType.STRING).description("시간 - 시작 시간"), + fieldWithPath("theme").type(JsonFieldType.OBJECT).description("테마"), + fieldWithPath("theme.id").type(JsonFieldType.NUMBER).description("테마 - 키"), + fieldWithPath("theme.name").type(JsonFieldType.STRING).description("테마 - 이름"), + fieldWithPath("theme.description").type(JsonFieldType.STRING).description("테마 - 설명"), + fieldWithPath("theme.thumbnail").type(JsonFieldType.STRING).description("테마 - 썸네일"), + fieldWithPath("status").type(JsonFieldType.STRING).description("예약 혹은 대기 상태")); + + Map params = Map.of( + "memberId", 1L, + "date", "9999-09-09", + "timeId", 1L, + "themeId", 1L); + + RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(requestCookies, requestFields, responseFields)) + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .cookie("token", ADMIN_TOKEN) + .body(params) + .when().post("/admin/reservations") + .then().log().all() + .statusCode(201); + } + + @Test + @DisplayName("예약 삭제") + void deleteReservation() { + RequestCookiesSnippet requestCookies = requestCookies( + cookieWithName("token").description("회원 인증 토큰 (어드민이어야 합니다)")); + Map params = Map.of( + "memberId", 1L, + "date", "9999-09-09", + "timeId", 1L, + "themeId", 1L, + "amount", 1000, + "orderId", "orderId", + "paymentKey", "paymentKey"); + String createdId = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .cookie("token", ADMIN_TOKEN) + .body(params) + .when().post("/admin/reservations") + .then().log().all() + .statusCode(201).extract().header("location").split("/")[2]; + + RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(requestCookies)) + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .cookie("token", ADMIN_TOKEN) + .when().delete("/admin/reservations/" + createdId) + .then().log().all() + .statusCode(204); + } + @DisplayName("어드민의 예약 CRUD") @TestFactory Stream dynamicAdminTestsFromCollection() { @@ -142,6 +261,7 @@ Stream dynamicAdminTestsFromCollection() { @DisplayName("유저의 예약 생성") @TestFactory Stream dynamicUserTestsFromCollection() { + mockPaymentConfirm(1000, "orderId", "paymentKey"); return Stream.of( dynamicTest("내 예약 목록을 조회한다.", () -> { userReservationSize = RestAssured.given().log().all() @@ -235,6 +355,7 @@ Stream dynamicUserTestsFromCollection() { @DisplayName("예약 대기 생성") @TestFactory Stream dynamicWaitTestsFromCollection() { + mockPaymentConfirm(1000, "orderId111", "paymentKey111"); return Stream.of( dynamicTest("내 예약 목록을 조회한다.", () -> { userReservationSize = RestAssured.given().log().all() @@ -251,8 +372,8 @@ Stream dynamicWaitTestsFromCollection() { "timeId", 1L, "themeId", 1L, "amount", 1000, - "orderId", "orderId", - "paymentKey", "paymentKey"); + "orderId", "orderId111", + "paymentKey", "paymentKey111"); userReservationId = RestAssured.given().log().all() .contentType(ContentType.JSON) @@ -343,4 +464,20 @@ Stream dynamicWaitTestsFromCollection() { }) ); } + + private void mockPaymentConfirm(int amount, String orderId, String paymentKey) { + PaymentConfirmRequest paymentRequest = new PaymentConfirmRequest(amount, orderId, paymentKey); + PaymentResponse paymentResponse = new PaymentResponse( + "mId", + paymentKey, + orderId, + PaymentStatus.DONE, + LocalDateTime.now(), + LocalDateTime.now(), + null, + amount); + doReturn(paymentResponse) + .when(paymentClient) + .confirmPayment(paymentRequest); + } } diff --git a/src/test/java/roomescape/controller/ReservationTimeControllerTest.java b/src/test/java/roomescape/controller/ReservationTimeControllerTest.java index 9b1bff021..8dbacc967 100644 --- a/src/test/java/roomescape/controller/ReservationTimeControllerTest.java +++ b/src/test/java/roomescape/controller/ReservationTimeControllerTest.java @@ -4,20 +4,95 @@ import io.restassured.http.ContentType; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; -import roomescape.IntegrationTestSupport; +import org.springframework.restdocs.cookies.RequestCookiesSnippet; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.RequestFieldsSnippet; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; +import roomescape.controller.config.ControllerTestSupport; import java.util.Map; import java.util.stream.Stream; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; -class ReservationTimeControllerTest extends IntegrationTestSupport { +class ReservationTimeControllerTest extends ControllerTestSupport { String createdId; int timeSize; + @Test + @DisplayName("예약 시간 목록 조회") + void showReservationTime() { + RequestCookiesSnippet requestCookies = requestCookies( + cookieWithName("token").description("회원 인증 토큰")); + ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("[]").type(JsonFieldType.ARRAY).description("응답 배열"), + fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("키"), + fieldWithPath("[].startAt").type(JsonFieldType.STRING).description("시간")); + RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(requestCookies, responseFields)) + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .cookie("token", ADMIN_TOKEN) + .when().get("/times") + .then().log().all() + .statusCode(200); + } + + @Test + @DisplayName("예약 시간 추가") + void saveReservationTime() { + RequestCookiesSnippet requestCookies = requestCookies( + cookieWithName("token").description("회원 인증 토큰 (어드민이어야 합니다)")); + RequestFieldsSnippet requestFields = requestFields( + fieldWithPath("startAt").type(JsonFieldType.STRING).description("시간")); + ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("키"), + fieldWithPath("startAt").type(JsonFieldType.STRING).description("시간")); + + Map param = Map.of("startAt", "12:12"); + RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(requestCookies, requestFields, responseFields)) + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .cookie("token", ADMIN_TOKEN) + .body(param) + .when().post("/admin/times") + .then().log().all() + .statusCode(201); + } + + @Test + @DisplayName("예약 시간 삭제") + void deleteReservationTime() { + RequestCookiesSnippet requestCookies = requestCookies( + cookieWithName("token").description("회원 인증 토큰 (어드민이어야 합니다)")); + + Map param = Map.of("startAt", "13:13"); + String createdId = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .cookie("token", ADMIN_TOKEN) + .body(param) + .when().post("/admin/times") + .then().log().all() + .statusCode(201).extract().header("location").split("/")[2]; + + RestAssured.given(specification).log().all() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .filter(makeDocumentFilter(requestCookies)) + .cookie("token", ADMIN_TOKEN) + .when().delete("/admin/times/" + createdId) + .then().log().all() + .statusCode(204); + } + @DisplayName("예약 시간 CRUD") @TestFactory Stream dynamicUserTestsFromCollection() { diff --git a/src/test/java/roomescape/controller/ThemeControllerTest.java b/src/test/java/roomescape/controller/ThemeControllerTest.java index 62e4ff160..fdcee014f 100644 --- a/src/test/java/roomescape/controller/ThemeControllerTest.java +++ b/src/test/java/roomescape/controller/ThemeControllerTest.java @@ -6,19 +6,137 @@ import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; -import roomescape.IntegrationTestSupport; +import org.springframework.restdocs.cookies.RequestCookiesSnippet; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.RequestFieldsSnippet; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; +import org.springframework.restdocs.request.QueryParametersSnippet; +import roomescape.controller.config.ControllerTestSupport; import java.util.Map; import java.util.stream.Stream; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; -class ThemeControllerTest extends IntegrationTestSupport { +class ThemeControllerTest extends ControllerTestSupport { String createdId; int themeSize; + @Test + @DisplayName("테마 목록 조회") + void showTheme() { + RequestCookiesSnippet requestCookies = requestCookies( + cookieWithName("token").description("회원 인증 토큰")); + ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("[]").type(JsonFieldType.ARRAY).description("응답 배열"), + fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("키"), + fieldWithPath("[].name").type(JsonFieldType.STRING).description("이름"), + fieldWithPath("[].description").type(JsonFieldType.STRING).description("설명"), + fieldWithPath("[].thumbnail").type(JsonFieldType.STRING).description("썸네일")); + + RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(requestCookies, responseFields)) + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .cookie("token", ADMIN_TOKEN) + .when().get("/themes") + .then().log().all() + .statusCode(200); + } + + @Test + @DisplayName("테마 추가") + void saveTheme() { + RequestCookiesSnippet requestCookies = requestCookies( + cookieWithName("token").description("회원 인증 토큰")); + RequestFieldsSnippet requestFields = requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("이름"), + fieldWithPath("description").type(JsonFieldType.STRING).description("설명"), + fieldWithPath("thumbnail").type(JsonFieldType.STRING).description("썸네일")); + ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("키"), + fieldWithPath("name").type(JsonFieldType.STRING).description("이름"), + fieldWithPath("description").type(JsonFieldType.STRING).description("설명"), + fieldWithPath("thumbnail").type(JsonFieldType.STRING).description("썸네일")); + + Map param = Map.of( + "name", "테마_테스트", + "description", "설명_테스트", + "thumbnail", "썸네일"); + RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(requestCookies, requestFields, responseFields)) + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .cookie("token", ADMIN_TOKEN) + .body(param) + .when().post("/admin/themes") + .then().log().all() + .statusCode(201); + } + + @Test + @DisplayName("테마 삭제") + void deleteTheme() { + RequestCookiesSnippet requestCookies = requestCookies( + cookieWithName("token").description("회원 인증 토큰 (어드민이어야 합니다)")); + + Map param = Map.of( + "name", "테마_테스트", + "description", "설명_테스트", + "thumbnail", "썸네일"); + String createdId = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .cookie("token", ADMIN_TOKEN) + .body(param) + .when().post("/admin/themes") + .then().log().all() + .statusCode(201).extract().header("location").split("/")[2]; + + RestAssured.given(specification).log().all() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .filter(makeDocumentFilter(requestCookies)) + .cookie("token", ADMIN_TOKEN) + .when().delete("/admin/themes/" + createdId) + .then().log().all() + .statusCode(204); + } + + @Test + @DisplayName("인기 테마 조회") + void showPopularTheme() { + QueryParametersSnippet request = queryParameters( + parameterWithName("startDate").description("시작 날짜"), + parameterWithName("endDate").description("끝 날짜"), + parameterWithName("limit").description("개수")); + ResponseFieldsSnippet response = responseFields( + fieldWithPath("[]").type(JsonFieldType.ARRAY).description("응답 배열"), + fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("키"), + fieldWithPath("[].name").type(JsonFieldType.STRING).description("이름"), + fieldWithPath("[].description").type(JsonFieldType.STRING).description("설명"), + fieldWithPath("[].thumbnail").type(JsonFieldType.STRING).description("썸네일")); + + Map params = Map.of( + "startDate", "2000-01-01", + "endDate", "9999-09-09", + "limit", "2"); + RestAssured.given(specification).log().all() + .filter(makeDocumentFilter(request, response)) + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .queryParams(params) + .when().get("/themes/popular") + .then().log().all() + .statusCode(200); + } + @DisplayName("테마 생성 조회") @TestFactory Stream dynamicUserTestsFromCollection() { diff --git a/src/test/java/roomescape/controller/config/ControllerTestSupport.java b/src/test/java/roomescape/controller/config/ControllerTestSupport.java new file mode 100644 index 000000000..3e14568d1 --- /dev/null +++ b/src/test/java/roomescape/controller/config/ControllerTestSupport.java @@ -0,0 +1,94 @@ +package roomescape.controller.config; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.filter.Filter; +import io.restassured.specification.RequestSpecification; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.cookies.RequestCookiesSnippet; +import org.springframework.restdocs.cookies.ResponseCookiesSnippet; +import org.springframework.restdocs.payload.RequestFieldsSnippet; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; +import org.springframework.restdocs.request.QueryParametersSnippet; +import roomescape.IntegrationTestSupport; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class ControllerTestSupport extends IntegrationTestSupport { + + protected RequestSpecification specification; + + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { + Filter filter = documentationConfiguration(restDocumentation) + .operationPreprocessors() + .withRequestDefaults( + modifyHeaders() + .remove("Host") + .remove("Content-Length"), + prettyPrint()) + .withResponseDefaults( + modifyHeaders() + .remove("Transfer-Encoding") + .remove("Date") + .remove("Keep-Alive") + .remove("Connection") + .remove("Content-Length"), + prettyPrint()); + this.specification = new RequestSpecBuilder() + .addFilter(filter) + .build(); + } + + protected Filter makeDocumentFilter(QueryParametersSnippet request, ResponseFieldsSnippet response) { + return document( + "{class-name}/{method-name}", + request, + response); + } + + protected Filter makeDocumentFilter(RequestCookiesSnippet request, ResponseFieldsSnippet response) { + return document( + "{class-name}/{method-name}", + request, + response); + } + + protected Filter makeDocumentFilter(RequestCookiesSnippet requestCookies, QueryParametersSnippet requestParams, ResponseFieldsSnippet response) { + return document( + "{class-name}/{method-name}", + requestCookies, + requestParams, + response); + } + + protected Filter makeDocumentFilter(RequestCookiesSnippet requestCookies, RequestFieldsSnippet requestFields, ResponseFieldsSnippet response) { + return document( + "{class-name}/{method-name}", + requestCookies, + requestFields, + response); + } + + protected Filter makeDocumentFilter(RequestCookiesSnippet request) { + return document( + "{class-name}/{method-name}", + request); + } + + protected Filter makeDocumentFilter(RequestFieldsSnippet request) { + return document( + "{class-name}/{method-name}", + request); + } + + protected Filter makeDocumentFilter(RequestFieldsSnippet request, ResponseCookiesSnippet responseCookies) { + return document( + "{class-name}/{method-name}", + request, + responseCookies); + } +} diff --git a/src/test/java/roomescape/domain/reservation/ReservationTest.java b/src/test/java/roomescape/domain/reservation/ReservationTest.java index 5e2d17287..ec9f2397c 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTest.java @@ -27,8 +27,9 @@ void addWaitingByDuplicateReservationMember() { ReservationTime time = new ReservationTime(LocalTime.parse("10:00")); Theme theme = new Theme("테마이름", "테마 상세", "테마 섬네일"); ReservationSlot reservationSlot = new ReservationSlot(date, time, theme); + PaymentInfo paymentInfo = new PaymentInfo("paymentKey", "orderId", 1000); - Reservation reservation = new Reservation(1L, member, reservationSlot); + Reservation reservation = new Reservation(1L, member, reservationSlot, paymentInfo); // when // then assertThatThrownBy(() -> reservation.addWaiting(member)) @@ -47,8 +48,9 @@ void addWaitingByDuplicateWaitingMember() { ReservationTime time = new ReservationTime(LocalTime.parse("10:00")); Theme theme = new Theme("테마이름", "테마 상세", "테마 섬네일"); ReservationSlot reservationSlot = new ReservationSlot(date, time, theme); + PaymentInfo paymentInfo = new PaymentInfo("paymentKey", "orderId", 1000); - Reservation reservation = new Reservation(1L, member, reservationSlot); + Reservation reservation = new Reservation(1L, member, reservationSlot, paymentInfo); reservation.addWaiting(member1); // when // then @@ -69,8 +71,9 @@ void addWaiting() { ReservationTime time = new ReservationTime(LocalTime.parse("10:00")); Theme theme = new Theme("테마이름", "테마 상세", "테마 섬네일"); ReservationSlot reservationSlot = new ReservationSlot(date, time, theme); + PaymentInfo paymentInfo = new PaymentInfo("paymentKey", "orderId", 1000); - Reservation reservation = new Reservation(1L, member, reservationSlot); + Reservation reservation = new Reservation(1L, member, reservationSlot, paymentInfo); // when Waiting waiting1 = reservation.addWaiting(member1); @@ -94,8 +97,9 @@ void approveEmptyWaiting() { ReservationTime time = new ReservationTime(LocalTime.parse("10:00")); Theme theme = new Theme("테마이름", "테마 상세", "테마 섬네일"); ReservationSlot reservationSlot = new ReservationSlot(date, time, theme); + PaymentInfo paymentInfo = new PaymentInfo("paymentKey", "orderId", 1000); - Reservation reservation = new Reservation(1L, member, reservationSlot); + Reservation reservation = new Reservation(1L, member, reservationSlot, paymentInfo); // when // then assertThatThrownBy(reservation::approveWaiting) @@ -115,8 +119,9 @@ void approveWaiting() { ReservationTime time = new ReservationTime(LocalTime.parse("10:00")); Theme theme = new Theme("테마이름", "테마 상세", "테마 섬네일"); ReservationSlot reservationSlot = new ReservationSlot(date, time, theme); + PaymentInfo paymentInfo = new PaymentInfo("paymentKey", "orderId", 1000); - Reservation reservation = new Reservation(1L, member, reservationSlot); + Reservation reservation = new Reservation(1L, member, reservationSlot, paymentInfo); reservation.addWaiting(member1); reservation.addWaiting(member2); diff --git a/src/test/java/roomescape/repository/ReservationRepositoryTest.java b/src/test/java/roomescape/repository/ReservationRepositoryTest.java index 3c8953ff3..db901e3de 100644 --- a/src/test/java/roomescape/repository/ReservationRepositoryTest.java +++ b/src/test/java/roomescape/repository/ReservationRepositoryTest.java @@ -12,8 +12,8 @@ import roomescape.domain.member.Member; import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservation.PaymentInfo; import roomescape.domain.reservation.ReservationRepository; -import roomescape.domain.reservation.WaitingRepository; import roomescape.domain.reservation.dto.ReservationReadOnly; import roomescape.domain.reservation.slot.*; import roomescape.service.dto.ReservationConditionRequest; @@ -53,10 +53,10 @@ void save() { Member member = Member.createUser("생강", "email@email.com", "1234"); Member savedMember = memberRepository.save(member); - ReservationSlot slot = new ReservationSlot(LocalDate.parse("2025-01-01"), savedReservationTime, - savedTheme); + ReservationSlot slot = new ReservationSlot(LocalDate.parse("2025-01-01"), savedReservationTime, savedTheme); + PaymentInfo paymentInfo = new PaymentInfo("paymentKey", "orderId", 1000); - Reservation reservation = new Reservation(savedMember, slot); + Reservation reservation = new Reservation(savedMember, slot, paymentInfo); Reservation savedReservation = reservationRepository.save(reservation); assertAll(() -> assertThat(savedReservation.getMember().getName()).isEqualTo("생강"), () -> assertThat(savedReservation.getSlot().getDate()).isEqualTo("2025-01-01"), @@ -75,7 +75,7 @@ void deleteExistById() { void findTimesByDateAndTheme() { // given Theme theme = themeRepository.findById(2L).get(); - LocalDate date = LocalDate.parse("2024-05-30"); + LocalDate date = LocalDate.parse("2024-06-30"); // when List times = reservationRepository.findTimesByDateAndTheme(date, theme); @@ -94,8 +94,8 @@ void findTimesByDateAndTheme() { @Test void findPopularThemes() { // given - LocalDate startDate = LocalDate.parse("2024-05-04"); - LocalDate endDate = LocalDate.parse("2024-05-30"); + LocalDate startDate = LocalDate.parse("2024-06-04"); + LocalDate endDate = LocalDate.parse("2024-06-30"); Limit limit = Limit.of(2); // when @@ -137,10 +137,10 @@ static Stream provideFilterCondition() { 4 ), Arguments.of( - new ReservationConditionRequest(null, null, LocalDate.parse("2024-05-09"), null), + new ReservationConditionRequest(null, null, LocalDate.parse("2024-06-09"), null), 9 ),Arguments.of( - new ReservationConditionRequest(null, null, null, LocalDate.parse("2024-05-09")), + new ReservationConditionRequest(null, null, null, LocalDate.parse("2024-06-09")), 11 ), Arguments.of( @@ -148,11 +148,11 @@ static Stream provideFilterCondition() { 4 ), Arguments.of( - new ReservationConditionRequest(2L, null, LocalDate.parse("2024-05-09"), null), + new ReservationConditionRequest(2L, null, LocalDate.parse("2024-06-09"), null), 6 ), Arguments.of( - new ReservationConditionRequest(1L, 1L, LocalDate.parse("2024-05-09"), LocalDate.parse("2024-05-30")), + new ReservationConditionRequest(1L, 1L, LocalDate.parse("2024-06-09"), LocalDate.parse("2024-06-30")), 1 ) ); diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java index 9ef0b1246..5f6605070 100644 --- a/src/test/java/roomescape/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -7,6 +7,8 @@ import roomescape.IntegrationTestSupport; import roomescape.domain.member.Member; import roomescape.domain.member.MemberRepository; +import roomescape.domain.payment.PaymentResponse; +import roomescape.domain.payment.PaymentStatus; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservation.slot.*; import roomescape.exception.PaymentException; @@ -14,11 +16,13 @@ import roomescape.service.dto.*; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.*; import static org.mockito.Mockito.doThrow; @Transactional @@ -42,14 +46,32 @@ class ReservationServiceTest extends IntegrationTestSupport { @DisplayName("예약 저장") @Test void saveReservation() { + // given ReservationTime time = reservationTimeRepository.save(new ReservationTime(LocalTime.parse("01:00"))); Theme theme = themeRepository.save(new Theme("이름", "설명", "썸네일")); Member member = memberRepository.save(Member.createUser("고구마", "email@email.com", "1234")); ReservationPaymentRequest reservationPaymentRequest = new ReservationPaymentRequest(member.getId(), LocalDate.parse("2025-11-11"), time.getId(), theme.getId(), 1000, "orderId", "paymentKey"); + + // when + PaymentConfirmRequest paymentRequest = new PaymentConfirmRequest(1000, "orderId", "paymentKey"); + PaymentResponse paymentResponse = new PaymentResponse( + "mId", + "paymentKey", + "orderId", + PaymentStatus.DONE, + LocalDateTime.now(), + LocalDateTime.now(), + null, + 1000); + doReturn(paymentResponse) + .when(paymentClient) + .confirmPayment(paymentRequest); + ReservationResponse reservationResponse = reservationService.saveReservation(reservationPaymentRequest); + // then assertAll( () -> assertThat(reservationResponse.member().name()).isEqualTo("고구마"), () -> assertThat(reservationResponse.date()).isEqualTo(LocalDate.parse("2025-11-11")), @@ -65,6 +87,7 @@ void saveReservation() { @DisplayName("결제 오류시 예약이 저장되지 않는다.") @Test void saveReservationPaymentError() { + // given LocalDate date = LocalDate.parse("2025-11-11"); ReservationTime time = reservationTimeRepository.save(new ReservationTime(LocalTime.parse("01:00"))); Theme theme = themeRepository.save(new Theme("이름", "설명", "썸네일")); @@ -73,12 +96,13 @@ void saveReservationPaymentError() { ReservationPaymentRequest reservationPaymentRequest = new ReservationPaymentRequest(member.getId(), date, time.getId(), theme.getId(), 1000, "orderId", "paymentKey"); + // when PaymentConfirmRequest paymentConfirmRequest = reservationPaymentRequest.toPaymentRequest(); - doThrow(PaymentException.class) .when(paymentClient) .confirmPayment(paymentConfirmRequest); + // then assertAll( () -> assertThatThrownBy(() -> reservationService.saveReservation(reservationPaymentRequest)) .isInstanceOf(PaymentException.class), @@ -100,6 +124,20 @@ void saveWaitReservation() { ReservationPaymentRequest reservationPaymentRequest2 = new ReservationPaymentRequest(member2.getId(), LocalDate.parse("2025-11-11"), time.getId(), theme.getId(), 1000, "orderId", "paymentKey"); + PaymentConfirmRequest paymentRequest = new PaymentConfirmRequest(1000, "orderId", "paymentKey"); + PaymentResponse paymentResponse = new PaymentResponse( + "mId", + "paymentKey", + "orderId", + PaymentStatus.DONE, + LocalDateTime.now(), + LocalDateTime.now(), + null, + 1000); + doReturn(paymentResponse) + .when(paymentClient) + .confirmPayment(paymentRequest); + ReservationResponse reservationResponse1 = reservationService.saveReservation(reservationPaymentRequest1); // when @@ -161,7 +199,7 @@ void deleteByAdminNotFound() { @Test void saveDuplicatedReservation() { ReservationPaymentRequest reservationPaymentRequest = new ReservationPaymentRequest( - 1L, LocalDate.parse("2024-05-04"), 1L, 1L, + 1L, LocalDate.parse("2024-06-04"), 1L, 1L, 1000, "orderId", "paymentKey"); assertThatThrownBy(() -> reservationService.saveReservation(reservationPaymentRequest)) @@ -172,7 +210,7 @@ void saveDuplicatedReservation() { @Test void findAllMyReservations() { // given // when - List allUserReservation = reservationService.findMyAllReservationAndWaiting(1L, LocalDate.parse("2024-05-30")); + List allUserReservation = reservationService.findMyAllReservationAndWaiting(1L, LocalDate.parse("2024-06-30")); // then assertThat(allUserReservation).hasSize(3) @@ -194,9 +232,9 @@ void findAllWaiting() { assertThat(allWaiting).hasSize(3) .extracting("name", "theme", "date", "startAt") .containsExactlyInAnyOrder( - tuple("유저2", "이름2", LocalDate.parse("2024-05-30"), LocalTime.parse("10:00")), - tuple("어드민", "이름2", LocalDate.parse("2024-05-30"), LocalTime.parse("10:00")), - tuple("어드민", "이름2", LocalDate.parse("2024-05-30"), LocalTime.parse("11:00")) + tuple("유저2", "이름2", LocalDate.parse("2024-06-30"), LocalTime.parse("10:00")), + tuple("어드민", "이름2", LocalDate.parse("2024-06-30"), LocalTime.parse("10:00")), + tuple("어드민", "이름2", LocalDate.parse("2024-06-30"), LocalTime.parse("11:00")) ); } @@ -217,6 +255,20 @@ void cancelReservation() { ReservationPaymentRequest reservationPaymentRequest3 = new ReservationPaymentRequest(member3.getId(), LocalDate.parse("2025-11-11"), time.getId(), theme.getId(), 1000, "orderId", "paymentKey"); + PaymentConfirmRequest paymentRequest = new PaymentConfirmRequest(1000, "orderId", "paymentKey"); + PaymentResponse paymentResponse = new PaymentResponse( + "mId", + "paymentKey", + "orderId", + PaymentStatus.DONE, + LocalDateTime.now(), + LocalDateTime.now(), + null, + 1000); + doReturn(paymentResponse) + .when(paymentClient) + .confirmPayment(paymentRequest); + Long reservationId = reservationService.saveReservation(reservationPaymentRequest1).id(); reservationService.saveReservation(reservationPaymentRequest2); reservationService.saveReservation(reservationPaymentRequest3); diff --git a/src/test/java/roomescape/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/service/ReservationTimeServiceTest.java index 21635bc86..1a9c0366e 100644 --- a/src/test/java/roomescape/service/ReservationTimeServiceTest.java +++ b/src/test/java/roomescape/service/ReservationTimeServiceTest.java @@ -78,7 +78,7 @@ void saveDuplicatedTime() { @Test void findBookedTimes() { // given - var reservationTimeBookedRequest = new ReservationTimeBookedRequest(LocalDate.parse("2024-05-04"), 1L); + var reservationTimeBookedRequest = new ReservationTimeBookedRequest(LocalDate.parse("2024-06-04"), 1L); // when List timesWithBooked = reservationTimeService.getTimesWithBooked( diff --git a/src/test/java/roomescape/service/ThemeServiceTest.java b/src/test/java/roomescape/service/ThemeServiceTest.java index 951a1ffea..6368784d9 100644 --- a/src/test/java/roomescape/service/ThemeServiceTest.java +++ b/src/test/java/roomescape/service/ThemeServiceTest.java @@ -7,6 +7,7 @@ import roomescape.IntegrationTestSupport; import roomescape.domain.member.Member; import roomescape.domain.member.MemberRepository; +import roomescape.domain.reservation.PaymentInfo; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservation.slot.*; @@ -94,7 +95,8 @@ void deleteExistReservation() { Theme theme = themeRepository.save(new Theme("이름", "설명", "썸네일")); Member member = memberRepository.save(Member.createUser("생강", "email@email.com", "1234")); ReservationSlot slot = new ReservationSlot(LocalDate.parse("2025-05-13"), time, theme); - reservationRepository.save(new Reservation(member, slot)); + PaymentInfo paymentInfo = new PaymentInfo("paymentKey", "orderId", 1000); + reservationRepository.save(new Reservation(member, slot, paymentInfo)); // when & then assertThatThrownBy(() -> themeService.deleteTheme(theme.getId())) @@ -105,7 +107,7 @@ void deleteExistReservation() { @Test void getPopularTheme() { // given - PopularThemeRequest popularThemeRequest = new PopularThemeRequest(LocalDate.parse("2024-05-04"), LocalDate.parse("2024-05-10"), 2); + PopularThemeRequest popularThemeRequest = new PopularThemeRequest(LocalDate.parse("2024-06-04"), LocalDate.parse("2024-06-10"), 2); // when List popularThemes = themeService.getPopularThemes(popularThemeRequest); diff --git a/src/test/resources/init.sql b/src/test/resources/init.sql new file mode 100644 index 000000000..2cfbafcd2 --- /dev/null +++ b/src/test/resources/init.sql @@ -0,0 +1,55 @@ +SET referential_integrity FALSE; +TRUNCATE TABLE reservation RESTART IDENTITY; +TRUNCATE TABLE waiting RESTART IDENTITY; +TRUNCATE TABLE theme RESTART IDENTITY; +TRUNCATE TABLE reservation_time RESTART IDENTITY; + +INSERT INTO theme (name, description, thumbnail) +VALUES ('이름1', '설명1', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'), + ('이름2', '설명2', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'), + ('이름3', '설명3', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'), + ('이름4', '설명4', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'), + ('이름5', '설명5', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'), + ('이름6', '설명6', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'), + ('이름7', '설명7', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'), + ('이름8', '설명8', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'), + ('이름9', '설명9', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'), + ('이름10', '설명10', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'), + ('이름11', '설명11', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'), + ('이름12', '설명12', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg'), + ('이름13', '설명13', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg') +; +INSERT INTO reservation_time (start_at) +VALUES ('09:00'), + ('10:00'), + ('11:00'), + ('12:00'), + ('13:00'), + ('14:00'), + ('15:00') +; +INSERT INTO reservation (member_id, date, time_id, theme_id, payment_key, order_id, amount, is_deleted) +VALUES (1, '2024-06-04', 1, 1, 'paymentKey1', 'orderId1', 1000, false), + (1, '2024-06-04', 2, 1, 'paymentKey2', 'orderId2', 1000, false), + (1, '2024-06-05', 3, 1, 'paymentKey3', 'orderId3', 1000, false), + (2, '2024-06-05', 1, 2, 'paymentKey4', 'orderId4', 1000, false), + (2, '2024-06-05', 1, 3, 'paymentKey5', 'orderId5', 1000, false), + (2, '2024-06-09', 1, 2, 'paymentKey6', 'orderId6', 1000, false), + (2, '2024-06-05', 1, 4, 'paymentKey7', 'orderId7', 1000, false), + (3, '2024-06-06', 1, 2, 'paymentKey8', 'orderId8', 1000, false), + (3, '2024-06-07', 1, 7, 'paymentKey9', 'orderId9', 1000, false), + (3, '2024-06-08', 1, 8, 'paymentKey10', 'orderId10', 1000, false), + (3, '2024-06-09', 1, 9, 'paymentKey11', 'orderId11', 1000, false), + (3, '2024-06-10', 1, 10, 'paymentKey12', 'orderId12', 1000, false), + (3, '2024-06-29', 2, 2, 'paymentKey13', 'orderId13', 1000, false), + (1, '2024-06-30', 1, 1, 'paymentKey14', 'orderId14', 1000, false), + (2, '2024-06-30', 2, 2, 'paymentKey15', 'orderId15', 1000, false), + (2, '2024-06-30', 3, 2, 'paymentKey16', 'orderId16', 1000, false), + (2, '2024-06-30', 4, 2, 'paymentKey17', 'orderId17', 1000, false), + (2, '2024-06-30', 7, 2, 'paymentKey18', 'orderId18', 1000, false) +; +INSERT INTO waiting (member_id, reservation_id, is_deleted) +VALUES (3, 15, false), + (1, 15, false), + (1, 16, false) +;