diff --git a/.gitignore b/.gitignore index d49a8b5..d616f1e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .gradle build .idea +*.yml + diff --git a/api/build.gradle b/api/build.gradle index 0f3feb0..fe5808e 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -17,6 +17,8 @@ java { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/api/src/main/java/org/onewayticket/config/AppConfig.java b/api/src/main/java/org/onewayticket/config/AppConfig.java new file mode 100644 index 0000000..7b8a9b2 --- /dev/null +++ b/api/src/main/java/org/onewayticket/config/AppConfig.java @@ -0,0 +1,18 @@ +package org.onewayticket.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + + /** + * TossPaymentController에서 사용되는 restTemplate + * + */ + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/api/src/main/java/org/onewayticket/controller/BookingController.java b/api/src/main/java/org/onewayticket/controller/BookingController.java index ec17a44..2ced03a 100644 --- a/api/src/main/java/org/onewayticket/controller/BookingController.java +++ b/api/src/main/java/org/onewayticket/controller/BookingController.java @@ -3,6 +3,9 @@ import lombok.RequiredArgsConstructor; import org.onewayticket.dto.BookingDetailsDto; import org.onewayticket.dto.BookingRequestDto; +import org.onewayticket.dto.BookingResponseDto; +import org.onewayticket.security.AuthService; +import org.onewayticket.service.BookingService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -15,78 +18,65 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.format.DateTimeParseException; - @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/bookings") public class BookingController { + private final BookingService bookingService; + private final AuthService authService; + // 예약 생성 @PostMapping - public ResponseEntity createBooking(@RequestBody BookingRequestDto bookingRequestInfo) { - if (bookingRequestInfo.paymentId() == null || bookingRequestInfo.paymentId().isBlank() || !bookingRequestInfo.paymentId().equals("Confirmed")) { + public ResponseEntity createBooking(@RequestBody BookingRequestDto bookingRequestInfo, + @PathVariable String flightId) { + + // 결제 정보 검증 + if (bookingRequestInfo.paymentKey() == null || bookingRequestInfo.paymentKey().isBlank()) { return ResponseEntity.status(400).build(); } - return ResponseEntity.ok(new BookingDetailsDto("A1234", bookingRequestInfo.bookingName(), bookingRequestInfo.bookingEmail(), bookingRequestInfo.bookingPhoneNumber(), - bookingRequestInfo.flightId(), "ICN", "NRT", LocalDate.of(2023, 12, 1), LocalDate.of(2023, 12, 1), "Jane Doe", - LocalDate.parse(bookingRequestInfo.birthDate()), 25, "Female", "AB123456", "Korean", "12A", "Economy", BigDecimal.valueOf(500), "Confirmed")); + bookingService.createBooking(bookingRequestInfo, flightId); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } @GetMapping - public ResponseEntity getBookingDetails( - @RequestParam("bookingId") String bookingId, - @RequestParam("name") String name, - @RequestParam("birthDate") String birthDate) { - - // 날짜 유효성 검증 - if (!isValidDate(birthDate)) { - return ResponseEntity.badRequest().body(null); - } + public ResponseEntity getBookingDetails( + @RequestParam("referenceCode") String referenceCode, + @RequestParam("bookingEmail") String bookingEmail) { - // 미리 정의된 예약 정보 - BookingDetailsDto bookingDetails = new BookingDetailsDto( - "B1234", "John Doe", "john.doe@example.com", "123456789", - "FL123", "ICN", "NRT", LocalDate.of(2023, 12, 1), LocalDate.of(2023, 12, 1), - name, LocalDate.parse(birthDate), 25, "Female", "AB123456", "Korean", "12A", "Economy", - BigDecimal.valueOf(500), "Confirmed"); + + BookingDetailsDto bookingDetailsDto = bookingService.getBookingDetails(referenceCode); // bookingId가 일치하지 않을 경우 예외 처리 - if (!bookingDetails.bookingId().equals(bookingId)) { + if (!bookingDetailsDto.referenceCode().equals(referenceCode)) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); } - return ResponseEntity.ok(bookingDetails); - } + String token = authService.generateToken(referenceCode, bookingEmail); + // record 인스턴스 생성 + BookingResponseDto responseDto = new BookingResponseDto(token, bookingDetailsDto); + + // DTO 반환 + return ResponseEntity.ok(responseDto); + + } // 예약 취소 @DeleteMapping("/{id}") public ResponseEntity cancelBooking( @PathVariable String id, - @RequestHeader(value = "Authorization", required = false) String authHeader) { + @RequestHeader(value = "Authorization", required = true) String token) { - // 토큰 검증 - if (authHeader == null || !authHeader.equals("Bearer VALID_TOKEN")) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); // 메시지 없이 401 반환 + try { + bookingService.cancelBooking(id, token); + } catch (Exception e) { + return ResponseEntity.status(400).build(); } - - // 200 OK와 함께 예약 ID 반환 return ResponseEntity.ok("Booking with ID " + id + " has been canceled successfully."); - } - - // 오늘 이후 날짜이거나 입력 값이 맞는지 확인 - private boolean isValidDate(String date) { - try { - LocalDate parsedDate = LocalDate.parse(date); // 기본 포맷 yyyy-MM-dd - return !parsedDate.isAfter(LocalDate.now()); // 오늘 이후 날짜인지 확인 - } catch (DateTimeParseException e) { - return false; // 형식이 잘못된 경우 false 반환 - } } diff --git a/api/src/main/java/org/onewayticket/controller/FlightController.java b/api/src/main/java/org/onewayticket/controller/FlightController.java index ea4457d..8b6b0c4 100644 --- a/api/src/main/java/org/onewayticket/controller/FlightController.java +++ b/api/src/main/java/org/onewayticket/controller/FlightController.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.onewayticket.dto.FlightDto; +import org.onewayticket.service.FlightService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -9,105 +10,50 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.math.BigDecimal; -import java.time.Duration; -import java.time.LocalDateTime; import java.util.List; +import java.util.NoSuchElementException; @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/flights") public class FlightController { + private final FlightService flightService; @GetMapping("/cheapest") public ResponseEntity> getCheapestFlights() { - // 더미 데이터 9개 생성 - List flights = List.of( - new FlightDto("FL001", BigDecimal.valueOf(50), LocalDateTime.now(), "ICN", "NRT", - LocalDateTime.now().plusHours(2), LocalDateTime.now().plusHours(4), Duration.ofHours(2)), - new FlightDto("FL002", BigDecimal.valueOf(70), LocalDateTime.now(), "ICN", "HND", - LocalDateTime.now().plusHours(3), LocalDateTime.now().plusHours(5), Duration.ofHours(2)), - new FlightDto("FL003", BigDecimal.valueOf(80), LocalDateTime.now(), "ICN", "KIX", - LocalDateTime.now().plusHours(4), LocalDateTime.now().plusHours(6), Duration.ofHours(2)), - new FlightDto("FL004", BigDecimal.valueOf(100), LocalDateTime.now(), "ICN", "LAX", - LocalDateTime.now().plusHours(5), LocalDateTime.now().plusHours(16), Duration.ofHours(11)), - new FlightDto("FL005", BigDecimal.valueOf(120), LocalDateTime.now(), "ICN", "JFK", - LocalDateTime.now().plusHours(6), LocalDateTime.now().plusHours(20), Duration.ofHours(14)), - new FlightDto("FL006", BigDecimal.valueOf(150), LocalDateTime.now(), "ICN", "CDG", - LocalDateTime.now().plusHours(7), LocalDateTime.now().plusHours(19), Duration.ofHours(12)), - new FlightDto("FL007", BigDecimal.valueOf(200), LocalDateTime.now(), "ICN", "SYD", - LocalDateTime.now().plusHours(8), LocalDateTime.now().plusHours(18), Duration.ofHours(10)), - new FlightDto("FL008", BigDecimal.valueOf(300), LocalDateTime.now(), "ICN", "FRA", - LocalDateTime.now().plusHours(9), LocalDateTime.now().plusHours(22), Duration.ofHours(13)), - new FlightDto("FL009", BigDecimal.valueOf(350), LocalDateTime.now(), "ICN", "SIN", - LocalDateTime.now().plusHours(10), LocalDateTime.now().plusHours(16), Duration.ofHours(6)) - ); - - // 가격 기준으로 정렬 후 상위 9개 반환 - return ResponseEntity.ok(flights.stream() - .sorted((f1, f2) -> f1.price().compareTo(f2.price())) - .limit(9) - .toList()); + List flightDtos = flightService.getCheapestFlights(); + return ResponseEntity.ok(flightDtos); } - // 검색결과 보여줄 필터 추가 @GetMapping("/search") public ResponseEntity> searchFlights( - @RequestParam("departure") String departure, + @RequestParam("origin") String origin, @RequestParam("destination") String destination, @RequestParam("departureDate") String departureDate, - @RequestParam("numberOfPassengers") Integer numberOfPassengers, @RequestParam(value = "sort", defaultValue = "price") String sort) { - // 출발지와 목적지가 같은 경우 - if (departure.equalsIgnoreCase(destination)) { - return ResponseEntity.badRequest().body(List.of()); - } - - // 예제 데이터 생성 - List flights = List.of( - new FlightDto("FL001", BigDecimal.valueOf(50), LocalDateTime.now(), departure, destination, - LocalDateTime.now().plusHours(2), LocalDateTime.now().plusHours(4), Duration.ofHours(2)), - new FlightDto("FL002", BigDecimal.valueOf(70), LocalDateTime.now(), departure, destination, - LocalDateTime.now().plusHours(3), LocalDateTime.now().plusHours(5), Duration.ofHours(2)), - new FlightDto("FL003", BigDecimal.valueOf(120), LocalDateTime.now(), departure, destination, - LocalDateTime.now().plusHours(4), LocalDateTime.now().plusHours(6), Duration.ofHours(2)) - ); - - // 정렬 로직 추가 - List sortedFlights = flights.stream() - .sorted((f1, f2) -> { - switch (sort) { - case "price": - default: - return f1.price().compareTo(f2.price()); - case "arrivalTime": - return f1.arrivalTime().compareTo(f2.arrivalTime()); - case "flightDuration": - return f1.flightDuration().compareTo(f2.flightDuration()); - - } - }) - .toList(); - - return ResponseEntity.ok(sortedFlights); + List flightDtoList = flightService.searchFlights(origin, destination, departureDate, sort); + return ResponseEntity.ok(flightDtoList); } @GetMapping("/{flightId}") - public ResponseEntity getFlightDetails(@PathVariable String flightId) { - if (flightId.length() > 50) { // flightId 유효성 검사 + public ResponseEntity getFlightDetails(@PathVariable String flightId) { + + // 유효성 검사 + if (flightId.length() > 50 || flightId == null || flightId.isBlank()) { return ResponseEntity.badRequest().build(); } - if ("FL001".equals(flightId)) { - return ResponseEntity.ok(new FlightDto( - "FL001", BigDecimal.valueOf(50), LocalDateTime.now(), "ICN", "NRT", - LocalDateTime.now().plusHours(2), LocalDateTime.now().plusHours(4), Duration.ofHours(2) - )); + try { + FlightDto flightDto = flightService.getFlightDetails(flightId); + return ResponseEntity.ok(flightDto); + } catch (NoSuchElementException e) { + // flightId에 해당하는 항공편이 없을 경우 404 Not Found 반환 + return ResponseEntity.notFound().build(); } - return ResponseEntity.notFound().build(); } +} + -} diff --git a/api/src/main/java/org/onewayticket/controller/TossPaymentController.java b/api/src/main/java/org/onewayticket/controller/TossPaymentController.java new file mode 100644 index 0000000..b5eab6b --- /dev/null +++ b/api/src/main/java/org/onewayticket/controller/TossPaymentController.java @@ -0,0 +1,139 @@ +package org.onewayticket.controller; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import org.onewayticket.dto.PaymentConfirmRequestDto; +import org.onewayticket.dto.PaymentRequestDto; +import org.onewayticket.dto.PaymentResponseDto; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/payments") +public class TossPaymentController { + + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + + /** + * 클라이언트에서 "결제하기" 버튼 클릭하면 결제 금액 변경사항 확인을 위해 + * 결제 금액을 세션에 임시 저장함. + */ + @PostMapping("/tempsave") + public ResponseEntity tempsave(HttpSession session, @RequestBody PaymentRequestDto paymentRequestDto) { + session.setAttribute(paymentRequestDto.orderId(), paymentRequestDto.amount()); + return ResponseEntity.ok("결제 정보가 세션에 임시로 저장되었습니다."); + } + + /** + * 결제 승인 전, 결제 금액을 검증 + */ + @PostMapping("/verifyAmount") + public ResponseEntity verifyAmount(HttpSession session, @RequestBody PaymentRequestDto paymentRequestDto) { + + String amount = (String) session.getAttribute(paymentRequestDto.orderId()); + + // 결제 요청 전 세션에 저장된 가격과 결제 승인 전 금액을 비교 + if (amount == null || !amount.equals(paymentRequestDto.amount())) + return ResponseEntity.badRequest().build(); + + // 검증에 사용했던 세션은 삭제 + session.removeAttribute(paymentRequestDto.orderId()); + + return ResponseEntity.ok("결제 금액이 일치합니다."); + } + + /** + * 토스에게 결제 승인 요청하는 api + * 클라이언트에게서 받은 결제 완료 요청에 시크릿 키를 더해 토스 서버로 보내는 요청 + */ + @PostMapping("/confirm") + public ResponseEntity confirmPayment(@RequestBody PaymentConfirmRequestDto paymentConfirmRequestDto) throws Exception { + + /** + * 토스 페이먼츠 API 요청시 필요한 비밀 키 + * 실제 secretKey로 교체 전 은닉 예정. + */ + String widgetSecretKey = "test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6"; + String authorizations = "Basic " + Base64.getEncoder().encodeToString((widgetSecretKey + ":").getBytes(StandardCharsets.UTF_8)); + + // 클라이언트에게서 받은 정보로 요청 데이터 생성 + Map requestMap = Map.of( + "orderId", paymentConfirmRequestDto.orderId(), + "amount", paymentConfirmRequestDto.amount(), + "paymentKey", paymentConfirmRequestDto.paymentKey() + ); + + String requestBody = objectMapper.writeValueAsString(requestMap); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", authorizations); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity requestEntity = new HttpEntity<>(objectMapper.writeValueAsString(requestMap), headers); + + String url = "https://api.tosspayments.com/v1/payments/confirm"; + /** + * toss에 요청 + * 성공 시 : DB에 저장, 성공 메시지 반환. + * 실패 시 : 실패 메시지 반환 + */ + try { + ResponseEntity response = + restTemplate.postForEntity(url, requestEntity, PaymentResponseDto.class); + if (response.getStatusCode().is2xxSuccessful()) { + // TODO: DB에 결제 정보 저장 + return ResponseEntity.ok(response.getBody()); + } else { + return ResponseEntity.status(response.getStatusCode()).body("결제에 실패하였습니다."); + } + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("알 수 없는 오류 발생: " + e.getMessage()); + } + + } + + /** + * 결제 취소 요청 + */ + @PostMapping("/cancel") + public ResponseEntity cancelPayment(String paymentKey, String cancelReason) { + // 요청 URL + String url = "https://api.tosspayments.com/v1/payments/" + paymentKey + "/cancel"; + + // Authorization 헤더 생성 + String secretKey = "test_sk_zXLkKEypNArWmo50nX3lmeaxYG5R"; // 실제 키로 교체 필요 + String authorization = "Basic " + Base64.getEncoder().encodeToString((secretKey + ":").getBytes(StandardCharsets.UTF_8)); + + // 요청 데이터 + Map requestBody = Map.of("cancelReason", cancelReason); + + // HTTP 헤더 설정 + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", authorization); + headers.setContentType(MediaType.APPLICATION_JSON); + + // HTTP 요청 본문 설정 + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + // RestTemplate로 요청 전송 + return restTemplate.postForEntity(url, requestEntity, String.class); + } + +} diff --git a/api/src/main/java/org/onewayticket/domain/Booking.java b/api/src/main/java/org/onewayticket/domain/Booking.java new file mode 100644 index 0000000..9c8ea11 --- /dev/null +++ b/api/src/main/java/org/onewayticket/domain/Booking.java @@ -0,0 +1,74 @@ +package org.onewayticket.domain; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.onewayticket.dto.BookingRequestDto; +import org.onewayticket.enums.BookingStatus; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Booking { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String referenceCode; // 사용자 조회용 예약 번호 + + private String bookingEmail; + + private Long flightId; + + private String paymentKey; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "booking_id") + @Builder.Default + private List passengers = new ArrayList<>(); + + @Enumerated(EnumType.STRING) + @Column(columnDefinition = "VARCHAR(50)") + private BookingStatus status; + + + private LocalDateTime createdAt; // 예약 시간 + + public static Booking from(BookingRequestDto dto, Long flightId) { + return Booking.builder() + .referenceCode(generateReferenceCode()) + .bookingEmail(dto.bookingEmail()) + .flightId(flightId) + .paymentKey(dto.paymentKey()) + .status(BookingStatus.PENDING) + .createdAt(LocalDateTime.now()) + .passengers(dto.passengers().stream() + .map(Passenger::from) + .toList()) + .build(); + } + + private static String generateReferenceCode() { + return UUID.randomUUID().toString().substring(0, 8); // 앞 8자리만 사용 + } + +} diff --git a/api/src/main/java/org/onewayticket/domain/Flight.java b/api/src/main/java/org/onewayticket/domain/Flight.java new file mode 100644 index 0000000..fb8f381 --- /dev/null +++ b/api/src/main/java/org/onewayticket/domain/Flight.java @@ -0,0 +1,35 @@ +package org.onewayticket.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Getter; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Getter +public class Flight { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String flightNumber; + + private BigDecimal amount; + + private LocalDateTime departureTime; + + private LocalDateTime arrivalTime; + + private String origin; + + private String destination; + + private int durationInMinutes; + + private String carrier; // 항공사 +} diff --git a/api/src/main/java/org/onewayticket/domain/Passenger.java b/api/src/main/java/org/onewayticket/domain/Passenger.java new file mode 100644 index 0000000..f9c785d --- /dev/null +++ b/api/src/main/java/org/onewayticket/domain/Passenger.java @@ -0,0 +1,42 @@ +package org.onewayticket.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.onewayticket.dto.PassengerDto; + +import java.time.LocalDate; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Passenger { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String referenceCode; + private String firstName; + private String lastName; + private String passportNumber; + private String gender; + private String seatNumber; + private LocalDate dateOfBirth; + + public static Passenger from(PassengerDto dto) { + return Passenger.builder() + .firstName(dto.firstName()) + .lastName(dto.lastName()) + .passportNumber(dto.passportNumber()) + .dateOfBirth(LocalDate.parse(dto.birthDate())) + .build(); + } + +} diff --git a/api/src/main/java/org/onewayticket/domain/TossPayment.java b/api/src/main/java/org/onewayticket/domain/TossPayment.java new file mode 100644 index 0000000..3393a9d --- /dev/null +++ b/api/src/main/java/org/onewayticket/domain/TossPayment.java @@ -0,0 +1,50 @@ +package org.onewayticket.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class TossPayment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long paymentId; + + @Column(nullable = false, unique = true) + private String tossPaymentKey; + + // 토스 내부 별도 orderId + @Column(nullable = false) + private String tossOrderId; + + private long totalAmount; + +// private Booking booking; + +// @Enumerated(value = EnumType.STRING) +// @Column(nullable = false) +// private TossPaymentMethod tossPaymentMethod; +// +// @Enumerated(value = EnumType.STRING) +// @Column(nullable = false) +// private TossPaymentStatus tossPaymentStatus; + + @Column(nullable = false) + private LocalDateTime requestedAt; + + +} \ No newline at end of file diff --git a/api/src/main/java/org/onewayticket/domain/User.java b/api/src/main/java/org/onewayticket/domain/User.java new file mode 100644 index 0000000..83d13e7 --- /dev/null +++ b/api/src/main/java/org/onewayticket/domain/User.java @@ -0,0 +1,27 @@ +package org.onewayticket.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "\"user\"") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true) + private String username; + + @Column(nullable = false) + private String password; + + @Column(nullable = false) + private String email; + +} diff --git a/api/src/main/java/org/onewayticket/dto/BookingDetailsDto.java b/api/src/main/java/org/onewayticket/dto/BookingDetailsDto.java index f5e069e..8750b2e 100644 --- a/api/src/main/java/org/onewayticket/dto/BookingDetailsDto.java +++ b/api/src/main/java/org/onewayticket/dto/BookingDetailsDto.java @@ -1,34 +1,12 @@ package org.onewayticket.dto; -import java.math.BigDecimal; -import java.time.LocalDate; +import java.util.List; public record BookingDetailsDto( - // 기본 예약 정보 - String bookingId, // 예약 ID - String bookingName, // 예약자 이름 + Long id, + String referenceCode, // 예약 번호(사용자 조회용) String bookingEmail, // 예약자 이메일 - String bookingPhoneNumber, // 예약자 전화번호 - - // 항공편 정보 - String flightId, // 항공편 ID - String flightOrigin, // 출발 공항 - String flightDestination, // 도착 공항 - LocalDate flightDepartureDate, // 출발 날짜 - LocalDate flightArrivalDate, // 도착 날짜 - - // 탑승자 정보 - String passengerName, // 탑승자 이름 - LocalDate passengerBirthDate, // 탑승자 생년월일 - int passengerAge, // 탑승자 나이 - String passengerGender, // 탑승자 성별 - String passengerPassportNumber, // 여권 번호 - String passengerNationality, // 국적 - String passengerSeatNumber, // 좌석 번호 - String passengerSeatClass, // 좌석 등급 (예: Economy, Business) - - // 결제 정보 - BigDecimal totalAmount, // 총 결제 금액 - String paymentStatus // 결제 상태 + FlightDto flightDto, // 항공편 정보 + List passengerDtoList// 탑승자 정보 ) { } diff --git a/api/src/main/java/org/onewayticket/dto/BookingDto.java b/api/src/main/java/org/onewayticket/dto/BookingDto.java deleted file mode 100644 index d47b8ef..0000000 --- a/api/src/main/java/org/onewayticket/dto/BookingDto.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.onewayticket.dto; - -public record BookingDto( - String bookingId, // 예약 ID - String reservationName, // 예약자 이름 - String reservationEmail, // 예약자 이메일 - String reservationPhoneNumber // 예약자 전화번호 -) { -} diff --git a/api/src/main/java/org/onewayticket/dto/BookingRequestDto.java b/api/src/main/java/org/onewayticket/dto/BookingRequestDto.java index f8003c0..06a129a 100644 --- a/api/src/main/java/org/onewayticket/dto/BookingRequestDto.java +++ b/api/src/main/java/org/onewayticket/dto/BookingRequestDto.java @@ -1,11 +1,10 @@ package org.onewayticket.dto; +import java.util.List; + public record BookingRequestDto( - String bookingName, String bookingEmail, - String bookingPhoneNumber, - String flightId, // 항공편명 - String birthDate, - String paymentId // 결제 id + List passengers, + String paymentKey // 결제 id ) { } diff --git a/api/src/main/java/org/onewayticket/dto/BookingResponseDto.java b/api/src/main/java/org/onewayticket/dto/BookingResponseDto.java new file mode 100644 index 0000000..c405949 --- /dev/null +++ b/api/src/main/java/org/onewayticket/dto/BookingResponseDto.java @@ -0,0 +1,7 @@ +package org.onewayticket.dto; + +public record BookingResponseDto( + String token, + BookingDetailsDto bookingDetails) +{ +} diff --git a/api/src/main/java/org/onewayticket/dto/FlightDto.java b/api/src/main/java/org/onewayticket/dto/FlightDto.java index e9907a3..f099ef7 100644 --- a/api/src/main/java/org/onewayticket/dto/FlightDto.java +++ b/api/src/main/java/org/onewayticket/dto/FlightDto.java @@ -1,18 +1,22 @@ package org.onewayticket.dto; +import lombok.Builder; + import java.math.BigDecimal; import java.time.Duration; import java.time.LocalDateTime; +@Builder public record FlightDto( - String flightId, // 항공편명 - BigDecimal price, // 항공권 가격 - LocalDateTime departureDate, // 출발 날짜 - String origin, // 출발 공항 코드 - String destination, // 도착 공항 코드 + Long id, // 항공편 id + String flightNumber, // 항공편명 + BigDecimal amount, // 항공권 가격 LocalDateTime departureTime, // 출발 시간 LocalDateTime arrivalTime, // 도착 시간 - Duration flightDuration // 소요 시간 + String origin, // 출발 공항 코드 + String destination, // 도착 공항 코드 + Duration flightDuration, // 소요 시간 + String carrier // 항공사 ) { } diff --git a/api/src/main/java/org/onewayticket/dto/PassengerDto.java b/api/src/main/java/org/onewayticket/dto/PassengerDto.java new file mode 100644 index 0000000..d6d287c --- /dev/null +++ b/api/src/main/java/org/onewayticket/dto/PassengerDto.java @@ -0,0 +1,11 @@ +package org.onewayticket.dto; + +public record PassengerDto( + String firstName, // 탑승자 성 + String lastName, // 탑승자 이름 + String birthDate, // 탑승자 생년월일 + String passportNumber, + String gender, + String seatNumber // 좌석 번호 +) { +} \ No newline at end of file diff --git a/api/src/main/java/org/onewayticket/dto/PaymentConfirmRequestDto.java b/api/src/main/java/org/onewayticket/dto/PaymentConfirmRequestDto.java new file mode 100644 index 0000000..7bf2478 --- /dev/null +++ b/api/src/main/java/org/onewayticket/dto/PaymentConfirmRequestDto.java @@ -0,0 +1,8 @@ +package org.onewayticket.dto; + +public record PaymentConfirmRequestDto( + String orderId, + String amount, + String paymentKey +) { +} diff --git a/api/src/main/java/org/onewayticket/dto/PaymentRequestDto.java b/api/src/main/java/org/onewayticket/dto/PaymentRequestDto.java new file mode 100644 index 0000000..43b6741 --- /dev/null +++ b/api/src/main/java/org/onewayticket/dto/PaymentRequestDto.java @@ -0,0 +1,9 @@ +package org.onewayticket.dto; + +import java.math.BigDecimal; + +public record PaymentRequestDto( + String orderId, + BigDecimal amount +) { +} diff --git a/api/src/main/java/org/onewayticket/dto/PaymentResponseDto.java b/api/src/main/java/org/onewayticket/dto/PaymentResponseDto.java new file mode 100644 index 0000000..3fd0118 --- /dev/null +++ b/api/src/main/java/org/onewayticket/dto/PaymentResponseDto.java @@ -0,0 +1,10 @@ +package org.onewayticket.dto; + +public record PaymentResponseDto( + String paymentKey, + String orderId, + String method, + String currency, + String totalAmount +) { +} diff --git a/api/src/main/java/org/onewayticket/dto/SaveAmountRequestDto.java b/api/src/main/java/org/onewayticket/dto/SaveAmountRequestDto.java new file mode 100644 index 0000000..f5d2ca1 --- /dev/null +++ b/api/src/main/java/org/onewayticket/dto/SaveAmountRequestDto.java @@ -0,0 +1,10 @@ +package org.onewayticket.dto; + + +import java.math.BigDecimal; + +public record SaveAmountRequestDto( + String orderId, + BigDecimal amount +) { +} diff --git a/api/src/main/java/org/onewayticket/enums/BookingStatus.java b/api/src/main/java/org/onewayticket/enums/BookingStatus.java new file mode 100644 index 0000000..f7fd05e --- /dev/null +++ b/api/src/main/java/org/onewayticket/enums/BookingStatus.java @@ -0,0 +1,8 @@ +package org.onewayticket.enums; + +public enum BookingStatus { + PENDING, + CONFIRMED, + CANCELLED, + COMPLETED; +} diff --git a/api/src/main/java/org/onewayticket/repository/BookingRepository.java b/api/src/main/java/org/onewayticket/repository/BookingRepository.java new file mode 100644 index 0000000..a3771f4 --- /dev/null +++ b/api/src/main/java/org/onewayticket/repository/BookingRepository.java @@ -0,0 +1,13 @@ +package org.onewayticket.repository; + +import org.onewayticket.domain.Booking; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface BookingRepository extends JpaRepository { + + Optional findByReferenceCode(String referenceCode); +} diff --git a/api/src/main/java/org/onewayticket/repository/FlightRepository.java b/api/src/main/java/org/onewayticket/repository/FlightRepository.java new file mode 100644 index 0000000..eddbfc8 --- /dev/null +++ b/api/src/main/java/org/onewayticket/repository/FlightRepository.java @@ -0,0 +1,32 @@ +package org.onewayticket.repository; + +import org.onewayticket.domain.Flight; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.List; + +@Repository +public interface FlightRepository extends JpaRepository { + + List findByOriginOrderByAmountAsc(String origin); + + @Query(""" + SELECT f FROM Flight f + WHERE (:origin IS NULL OR f.origin = :origin) + AND (:destination IS NULL OR f.destination = :destination) + AND (:departureDate IS NULL OR CAST(f.departureTime AS date) = :departureDate) + """) + List searchFlights( + @Param("origin") String origin, + @Param("destination") String destination, + @Param("departureDate") LocalDate departureDate + ); + + + + +} diff --git a/api/src/main/java/org/onewayticket/repository/PassengerRepository.java b/api/src/main/java/org/onewayticket/repository/PassengerRepository.java new file mode 100644 index 0000000..7f36bba --- /dev/null +++ b/api/src/main/java/org/onewayticket/repository/PassengerRepository.java @@ -0,0 +1,13 @@ +package org.onewayticket.repository; + +import org.onewayticket.domain.Booking; +import org.onewayticket.domain.Passenger; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface PassengerRepository extends JpaRepository { + List findByReferenceCode(String referenceCode); +} diff --git a/api/src/main/java/org/onewayticket/security/AuthService.java b/api/src/main/java/org/onewayticket/security/AuthService.java new file mode 100644 index 0000000..cbc0274 --- /dev/null +++ b/api/src/main/java/org/onewayticket/security/AuthService.java @@ -0,0 +1,14 @@ +package org.onewayticket.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthService { + + public String generateToken(String referenceCode, String bookingEmail) { + String token = TokenProvider.generateToken(referenceCode, bookingEmail); + return token; + } +} diff --git a/api/src/main/java/org/onewayticket/security/TokenProvider.java b/api/src/main/java/org/onewayticket/security/TokenProvider.java new file mode 100644 index 0000000..3127b9b --- /dev/null +++ b/api/src/main/java/org/onewayticket/security/TokenProvider.java @@ -0,0 +1,31 @@ +package org.onewayticket.security; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class TokenProvider { + + /** + * 예약번호와 이메일 기반으로 해시 토큰 생성 + */ + public static String generateToken(String reservationNumber, String email) { + try { + String data = reservationNumber + email; + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(data.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Error generating token", e); + } + } +} + diff --git a/api/src/main/java/org/onewayticket/service/BookingService.java b/api/src/main/java/org/onewayticket/service/BookingService.java new file mode 100644 index 0000000..039e494 --- /dev/null +++ b/api/src/main/java/org/onewayticket/service/BookingService.java @@ -0,0 +1,90 @@ +package org.onewayticket.service; + +import lombok.RequiredArgsConstructor; +import org.onewayticket.domain.Booking; +import org.onewayticket.domain.Flight; +import org.onewayticket.dto.BookingDetailsDto; +import org.onewayticket.dto.BookingRequestDto; +import org.onewayticket.dto.FlightDto; +import org.onewayticket.dto.PassengerDto; +import org.onewayticket.repository.BookingRepository; +import org.onewayticket.repository.FlightRepository; +import org.onewayticket.repository.PassengerRepository; +import org.onewayticket.security.AuthService; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class BookingService { + + private final BookingRepository bookingRepository; + private final FlightRepository flightRepository; + private final PassengerRepository passengerRepository; + private final AuthService authService; + + public void createBooking(BookingRequestDto bookingRequestInfo, String flightId) { + Booking booking = Booking.from(bookingRequestInfo, Long.valueOf(flightId)); + try { + bookingRepository.save(booking); + } catch (Exception e) { + throw new RuntimeException("Booking 저장 중 예외가 발생했습니다: " + e.getMessage(), e); + } + + } + + public boolean cancelBooking(String id, String token) { + Booking booking = bookingRepository.findById(Long.valueOf(id)) + .orElseThrow(() -> new IllegalArgumentException("해당 예약 정보가 없습니다.")); + String expectedToken = authService.generateToken(booking.getReferenceCode(), booking.getBookingEmail()); + if (expectedToken.equals(token)) { + bookingRepository.delete(booking); + return true; + } else { + throw new IllegalArgumentException("유효하지 않은 토큰입니다."); + } + + } + + public BookingDetailsDto getBookingDetails(String referenceCode) { + // Booking 조회 + Booking booking = bookingRepository.findByReferenceCode(referenceCode) + .orElseThrow(() -> new RuntimeException("Booking not found")); + + /** + * Flight 조회 및 DTO 변환 + * mapping 관계 고민 + */ + Flight flight = flightRepository.findById(booking.getFlightId()) + .orElseThrow(() -> new RuntimeException("Flight not found")); + + FlightDto flightDto = new FlightDto(flight.getId(), flight.getFlightNumber(), + flight.getAmount(), flight.getDepartureTime(), flight.getArrivalTime(), + flight.getOrigin(), flight.getDestination(), Duration.ofMinutes(flight.getDurationInMinutes()), flight.getCarrier()); + + + // Passenger DTO 변환 + List passengerDtoList = booking.getPassengers() + .stream() + .map(passenger -> new PassengerDto( + passenger.getFirstName(), + passenger.getLastName(), + passenger.getDateOfBirth().toString(), + passenger.getPassportNumber(), + passenger.getGender(), + passenger.getSeatNumber() + )) + .toList(); + + // BookingDetailsDto 반환 + return new BookingDetailsDto( + booking.getId(), + booking.getReferenceCode(), + booking.getBookingEmail(), + flightDto, + passengerDtoList + ); + } +} diff --git a/api/src/main/java/org/onewayticket/service/FlightService.java b/api/src/main/java/org/onewayticket/service/FlightService.java new file mode 100644 index 0000000..a6eb4fd --- /dev/null +++ b/api/src/main/java/org/onewayticket/service/FlightService.java @@ -0,0 +1,68 @@ +package org.onewayticket.service; + +import lombok.RequiredArgsConstructor; +import org.onewayticket.domain.Flight; +import org.onewayticket.dto.FlightDto; +import org.onewayticket.repository.FlightRepository; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.time.LocalDate; +import java.util.Comparator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class FlightService { + private final FlightRepository flightRepository; + + public FlightDto getFlightDetails(String flightId) { + Flight flight = flightRepository.findById(Long.valueOf(flightId)).orElseThrow(() -> + new NoSuchElementException("Flight not found with id: " + flightId)); + + return this.convertToDto(flight); + } + + public List getCheapestFlights() { + String origin = "ICN"; + List flights = flightRepository.findByOriginOrderByAmountAsc(origin); + return flights.stream() + .map(this::convertToDto) + .collect(Collectors.toList()); + } + + public List searchFlights(String origin, String destination, String departureDate, String sort) { + LocalDate parsedDate = LocalDate.parse(departureDate); + + List flights = flightRepository.searchFlights(origin, destination, parsedDate); + + flights.sort(getComparatorForSort(sort)); + + return flights.stream().map(this::convertToDto).collect(Collectors.toList()); + } + + private Comparator getComparatorForSort(String sort) { + return switch (sort) { + case "price" -> Comparator.comparing(Flight::getAmount); + case "arrivaltime" -> Comparator.comparing(Flight::getArrivalTime); + case "flightduration" -> Comparator.comparing(f -> Duration.ofMinutes(f.getDurationInMinutes())); + default -> throw new IllegalArgumentException("잘못된 정렬 방식입니다."); + }; + } + + private FlightDto convertToDto(Flight flight) { + return FlightDto.builder() + .id(flight.getId()) + .flightNumber(flight.getFlightNumber()) + .amount(flight.getAmount()) + .departureTime(flight.getDepartureTime()) + .arrivalTime(flight.getArrivalTime()) + .origin(flight.getOrigin()) + .destination(flight.getDestination()) + .flightDuration(Duration.ofMinutes(flight.getDurationInMinutes())) + .carrier(flight.getCarrier()) + .build(); + } +} diff --git a/api/src/test/java/org/onewayticket/controller/BookingControllerIntegrationTest.java b/api/src/test/java/org/onewayticket/controller/BookingControllerIntegrationTest.java index a5ee7fc..9707a32 100644 --- a/api/src/test/java/org/onewayticket/controller/BookingControllerIntegrationTest.java +++ b/api/src/test/java/org/onewayticket/controller/BookingControllerIntegrationTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import org.onewayticket.dto.BookingDetailsDto; import org.onewayticket.dto.BookingRequestDto; +import org.onewayticket.dto.PassengerDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; @@ -14,14 +15,17 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.test.context.jdbc.Sql; + +import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Sql(scripts = "/import.sql") class BookingControllerIntegrationTest { - @LocalServerPort private int port; @@ -41,8 +45,9 @@ void Create_booking_with_validate_info() { // Given String url = baseUrl + "/api/v1/bookings"; BookingRequestDto request = new BookingRequestDto( - "John Doe", "john.doe@example.com", "123456789", "FL123", "1995-05-01" - , "Confirmed" + "john.doe@example.com", + List.of(new PassengerDto("John", "Doe", "1995-05-01", "M34993022", "male", "12A")), // 탑승자 정보 + "paymentKey" // 결제 ID ); // When @@ -51,7 +56,7 @@ void Create_booking_with_validate_info() { // Then assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); - assertEquals("John Doe", response.getBody().bookingName()); + assertEquals("john.doe@example.com", response.getBody().bookingEmail()); } @Test @@ -60,8 +65,9 @@ void Create_Booking_With_Invalid_PaymentId() { // Given String url = baseUrl + "/api/v1/bookings"; BookingRequestDto request = new BookingRequestDto( - "John Doe", "john.doe@example.com", "123456789", "FL123", - "1995-05-01", "INVALID*ID" // 유효하지 않은 결제 ID + "john.doe@example.com", + List.of(new PassengerDto("John", "Doe", "1995-05-01", "M34993022", "male", "12A")), // 탑승자 정보 + "" // 결제 ID ); // When @@ -72,10 +78,10 @@ void Create_Booking_With_Invalid_PaymentId() { } @Test - @DisplayName("기존 예약 고객은 예약번호, 이름, 생년월일을 통해 예약정보를 조회할 수 있습니다.") + @DisplayName("기존 예약 고객은 예약번호, 이메일, 항공편 정보를 통해 예약정보를 조회할 수 있습니다.") void Get_bookingDetails_with_valid_info() { // Given - String url = baseUrl + "/api/v1/bookings?bookingId=B1234&name=John Doe&birthDate=1995-05-01"; + String url = baseUrl + "/api/v1/bookings?referenceCode=B1234&bookingEmail=\"john.doe@example.com\""; // When ResponseEntity response = restTemplate.getForEntity(url, BookingDetailsDto.class); @@ -83,15 +89,14 @@ void Get_bookingDetails_with_valid_info() { // Then assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); - assertEquals("B1234", response.getBody().bookingId()); - assertEquals("John Doe", response.getBody().passengerName()); + assertEquals("B1234", response.getBody().referenceCode()); } @Test @DisplayName("존재하지 않는 예약 정보로 조회할 때는 404를 반환합니다.") void Get_bookingDetails_with_invalid_info_not_found() { // Given - String url = baseUrl + "/api/v1/bookings?bookingId=B9999&name=John Smith&birthDate=1980-01-01"; + String url = baseUrl + "?referenceCode=B12345&bookingEmail=\"john.doe@example.com\""; // When ResponseEntity response = restTemplate.getForEntity(url, Void.class); @@ -100,24 +105,12 @@ void Get_bookingDetails_with_invalid_info_not_found() { assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); } - @Test - @DisplayName("잘못된 입력값으로 예약 조회할 때는 400을 반환합니다.") - void Get_bookingDetails_with_invalid_input_bad_request() { - // Given - String url = baseUrl + "/api/v1/bookings?bookingId=&name=&birthDate=9999-01-01"; - - // When - ResponseEntity response = restTemplate.getForEntity(url, Void.class); - - // Then - assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - } - @Test @DisplayName("사용자의 정보로 생성된 유효한 토큰으로 예약을 취소할 수 있습니다.") void Delete_booking_with_valid_token() { // Given - String url = baseUrl + "/api/v1/bookings/123"; + Long bookingId = 2L; + String url = baseUrl + "/api/v1/bookings" + bookingId; HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer VALID_TOKEN"); diff --git a/api/src/test/java/org/onewayticket/controller/FlightControllerIntegrationTest.java b/api/src/test/java/org/onewayticket/controller/FlightControllerIntegrationTest.java index 2727f94..c87152b 100644 --- a/api/src/test/java/org/onewayticket/controller/FlightControllerIntegrationTest.java +++ b/api/src/test/java/org/onewayticket/controller/FlightControllerIntegrationTest.java @@ -10,14 +10,16 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.test.context.jdbc.Sql; -import java.time.LocalDate; +import java.util.Arrays; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Sql(scripts = "/import.sql") public class FlightControllerIntegrationTest { @LocalServerPort private int port; @@ -45,7 +47,7 @@ void Get_cheapest_flights() { assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); assertTrue(response.getBody().length > 0); - assertTrue(response.getBody()[0].price().compareTo(response.getBody()[1].price()) <= 0); + assertTrue(response.getBody()[0].amount().compareTo(response.getBody()[1].amount()) <= 0); } @@ -53,26 +55,26 @@ void Get_cheapest_flights() { @DisplayName("입력한 출발지와 목적지에 해당하는 항공편이 리턴됩니다.") void Search_flights_by_valid_info() { // Given - String todayDate = LocalDate.now().toString(); // 현재 날짜를 "yyyy-MM-dd" 형식으로 가져옴 - String url = baseUrl + "/api/v1/flights/search?departure=ICN&destination=NRT&departureDate=" + todayDate + "&numberOfPassengers=1&sort=price"; + String todayDate = "2024-12-05"; + String url = baseUrl + "/api/v1/flights/search?origin=ICN&destination=NRT&departureDate=" + todayDate + "&sort=price"; // When ResponseEntity response = restTemplate.getForEntity(url, FlightDto[].class); // Then + System.out.println(Arrays.toString(response.getBody())); assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); assertEquals("ICN", response.getBody()[0].origin()); assertEquals("NRT", response.getBody()[0].destination()); assertEquals(todayDate, response.getBody()[0].departureTime().toLocalDate().toString()); - assertTrue(response.getBody()[0].price().compareTo(response.getBody()[1].price()) <= 0); } @Test @DisplayName("가격 정렬 필터를 넣으면 최저가 순으로 정렬되어 반환됩니다.") void Search_flights_sorted_by_price() { // Given - String url = baseUrl + "/api/v1/flights/search?departure=ICN&destination=NRT&departureDate=2023-12-01&numberOfPassengers=1&sort=price"; + String url = baseUrl + "/api/v1/flights/search?origin=ICN&destination=NRT&departureDate=2024-12-05&sort=price"; // When ResponseEntity response = restTemplate.getForEntity(url, FlightDto[].class); @@ -80,14 +82,14 @@ void Search_flights_sorted_by_price() { // Then assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); - assertTrue(response.getBody()[0].price().compareTo(response.getBody()[1].price()) <= 0); + assertTrue(response.getBody()[0].amount().compareTo(response.getBody()[1].amount()) <= 0); } @Test @DisplayName("도착시간 필터를 넣으면 가장 빠른 도착시간 순으로 정렬되어 반환됩니다.") void Search_flights_sorted_By_arrivaltime() { // Given - String url = baseUrl + "/api/v1/flights/search?departure=ICN&destination=NRT&departureDate=2023-12-01&numberOfPassengers=1&sort=arrivalTime"; + String url = baseUrl + "/api/v1/flights/search?origin=ICN&destination=NRT&departureDate=2024-12-05&sort=arrivalTime"; // When ResponseEntity response = restTemplate.getForEntity(url, FlightDto[].class); @@ -102,11 +104,12 @@ void Search_flights_sorted_By_arrivaltime() { @DisplayName("최소 비행시간 필터를 넣으면 가장 짧은 비행시간 순으로 정렬되어 반환됩니다.") void search_flights_sorted_by_flight_duration() { // Given - String url = baseUrl + "/api/v1/flights/search?departure=ICN&destination=NRT&departureDate=2023-12-01&numberOfPassengers=1&sort=flightDuration"; + String url = baseUrl + "/api/v1/flights/search?origin=ICN&destination=NRT&departureDate=2024-12-05&sort=flightDuration"; // When ResponseEntity response = restTemplate.getForEntity(url, FlightDto[].class); + System.out.println(Arrays.toString(response.getBody())); // Then assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); @@ -118,7 +121,8 @@ void search_flights_sorted_by_flight_duration() { @DisplayName("유효한 flightId로 항공권 조회가 가능합니다.") void Get_flightDetails_with_valid_flightId() { // Given - String url = baseUrl + "/api/v1/flights/FL001"; + Long flightId = 1L; + String url = baseUrl + "/api/v1/flights/" + flightId; // When ResponseEntity response = restTemplate.getForEntity(url, FlightDto.class); @@ -126,7 +130,7 @@ void Get_flightDetails_with_valid_flightId() { // Then assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); - assertEquals("FL001", response.getBody().flightId()); + assertEquals("AA101", response.getBody().flightNumber()); } @Test diff --git a/api/src/test/resources/import.sql b/api/src/test/resources/import.sql new file mode 100644 index 0000000..3166cd1 --- /dev/null +++ b/api/src/test/resources/import.sql @@ -0,0 +1,42 @@ +INSERT INTO flight (flight_number, amount, departure_time, arrival_time, origin, destination, duration_in_minutes, carrier) VALUES +('AA101', 150.00, '2024-12-01 08:00:00', '2024-12-01 11:00:00', 'ICN', 'LAX', 180, 'American Airlines'), +('UA202', 200.00, '2024-12-01 09:00:00', '2024-12-01 13:00:00', 'ICN', 'ORD', 240, 'United Airlines'), +('DL303', 175.50, '2024-12-02 14:00:00', '2024-12-02 18:00:00', 'ICN', 'SEA', 240, 'Delta Airlines'), +('SW404', 100.75, '2024-12-03 06:00:00', '2024-12-03 08:30:00', 'ICN', 'HOU', 150, 'Southwest Airlines'), +('BA505', 300.00, '2024-12-04 19:00:00', '2024-12-05 07:00:00', 'ICN', 'JFK', 600, 'British Airways'), +('LH606', 400.00, '2024-12-05 20:00:00', '2024-12-06 10:00:00', 'ICN', 'NRT', 840, 'Lufthansa'), +('AF707', 250.00, '2024-12-06 22:00:00', '2024-12-07 12:00:00', 'ICN', 'SIN', 720, 'Air France'), +('QF808', 350.00, '2024-12-07 11:00:00', '2024-12-07 23:00:00', 'ICN', 'DXB', 720, 'Qantas'), +('EK909', 500.00, '2024-12-08 15:00:00', '2024-12-09 05:00:00', 'ICN', 'JFK', 840, 'Emirates'), +('SQ1010', 450.00, '2024-12-09 16:00:00', '2024-12-10 08:00:00', 'ICN', 'SFO', 960, 'Singapore Airlines'), + +('AA102', 140.00, '2024-12-01 10:00:00', '2024-12-01 13:00:00', 'ICN', 'LAX', 180, 'American Airlines'), +('UA203', 210.00, '2024-12-01 11:00:00', '2024-12-01 15:00:00', 'ICN', 'ORD', 240, 'United Airlines'), +('DL304', 165.00, '2024-12-02 12:00:00', '2024-12-02 16:30:00', 'ICN', 'SEA', 270, 'Delta Airlines'), +('SW405', 120.00, '2024-12-03 07:00:00', '2024-12-03 10:00:00', 'ICN', 'HOU', 180, 'Southwest Airlines'), +('BA506', 280.00, '2024-12-04 20:00:00', '2024-12-05 08:30:00', 'ICN', 'JFK', 630, 'British Airways'), +('LH607', 390.00, '2024-12-05 22:00:00', '2024-12-06 12:00:00', 'ICN', 'NRT', 840, 'Lufthansa'), +('AF708', 260.00, '2024-12-07 00:00:00', '2024-12-07 14:00:00', 'ICN', 'SIN', 840, 'Air France'), +('QF809', 340.00, '2024-12-07 13:00:00', '2024-12-07 23:59:00', 'ICN', 'DXB', 659, 'Qantas'), +('EK910', 480.00, '2024-12-09 17:00:00', '2024-12-10 06:00:00', 'ICN', 'JFK', 780, 'Emirates'), +('SQ1011', 470.00, '2024-12-10 18:00:00', '2024-12-11 09:30:00', 'ICN', 'SFO', 930, 'Singapore Airlines'); + +INSERT INTO Booking (reference_code, booking_email, flight_id, payment_id, status, created_at) +VALUES +('B1234', 'johndoe@example.com', 123, 456, 'CONFIRMED', '2023-11-25 14:30:00'), +('B1235', 'alice@example.com', 124, 457, 'CONFIRMED', '2023-11-26 09:00:00'), +('B1236', 'bob@example.com', 125, 458, 'PENDING', '2023-11-27 13:45:00'), +('B1237', 'charlie@example.com', 126, 459, 'CANCELLED', '2023-11-28 16:20:00'), +('B1238', 'eve@example.com', 127, 460, 'CONFIRMED', '2023-11-29 10:10:00'); + +INSERT INTO Passenger (reference_code, first_name, last_name, passport_number, gender, seat_number, date_of_birth, booking_id) +VALUES +('P1234', 'John', 'Doe', 'A12345678', 'Male', '12A', '1995-05-26', 1), +('P1235', 'Jane', 'Doe', 'B98765432', 'Female', '12B', '1998-03-14', 1), +('P1236', 'Alice', 'Smith', 'C87654321', 'Female', '14A', '1992-08-15', 2), +('P1237', 'Bob', 'Smith', 'C98765432', 'Male', '14B', '1990-06-21', 2), +('P1238', 'Bob', 'Brown', 'D12345678', 'Male', '15C', '1985-03-12', 3), +('P1239', 'Charlie', 'Johnson', 'E12345678', 'Male', '16A', '1993-01-10', 4), +('P1240', 'Emily', 'Johnson', 'E87654321', 'Female', '16B', '1994-12-05', 4), +('P1241', 'Eve', 'Davis', 'F12345678', 'Female', '17A', '1988-11-22', 5), +('P1242', 'Oscar', 'Davis', 'F87654321', 'Male', '17B', '1987-04-14', 5);