From 32b86a7eeb4881dba7c523c4ce03735e4b0eb838 Mon Sep 17 00:00:00 2001 From: hyowon Date: Fri, 15 Aug 2025 01:36:59 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[feat/#73]=20=EC=98=88=EC=95=BD=20=EB=B6=88?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=9C=20=EB=82=A0=EC=A7=9C=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=20api=20=EA=B5=AC=ED=98=84(=EA=B2=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=ED=95=98=EC=9A=B0=EC=8A=A4=20=EA=B8=B0=EC=A4=80,?= =?UTF-8?q?=EA=B0=9D=EC=8B=A4=20=EA=B8=B0=EC=A4=80=20=EB=91=90=EA=B0=9C?= =?UTF-8?q?=EC=9D=98=20api)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ReservationRepository.java | 38 ++++++++++++ .../sumte/room/controller/RoomController.java | 20 +++++++ .../sumte/room/repository/RoomRepository.java | 1 + .../sumte/room/service/RoomQueryService.java | 2 + .../room/service/RoomQueryServiceImpl.java | 60 ++++++++++++++++++- 5 files changed, 118 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/sumte/reservation/repository/ReservationRepository.java b/src/main/java/com/sumte/reservation/repository/ReservationRepository.java index 41e3015..47dae26 100644 --- a/src/main/java/com/sumte/reservation/repository/ReservationRepository.java +++ b/src/main/java/com/sumte/reservation/repository/ReservationRepository.java @@ -40,4 +40,42 @@ and exists ( """) Page findAllPaidByUser(@Param("user") User user, Pageable pageable); + @Query(""" + select r from Reservation r + where r.room.guesthouse.id = :guesthouseId + and r.reservationStatus <> com.sumte.reservation.entity.ReservationStatus.CANCELED + and exists ( + select 1 + from Payment p + where p.reservation = r + and p.paymentStatus = com.sumte.payment.entity.PaymentStatus.PAID + ) + and r.startDate < :endExclusive + and r.endDate > :startInclusive + """) + List findActivePaidByGuesthouseAndOverlap( + @Param("guesthouseId") Long guesthouseId, + @Param("startInclusive") java.time.LocalDate startInclusive, + @Param("endExclusive") java.time.LocalDate endExclusive + ); + + @Query(""" + select r from Reservation r + where r.room = :room + and r.reservationStatus <> com.sumte.reservation.entity.ReservationStatus.CANCELED + and exists ( + select 1 + from Payment p + where p.reservation = r + and p.paymentStatus = com.sumte.payment.entity.PaymentStatus.PAID + ) + and r.startDate < :endExclusive + and r.endDate > :startInclusive + """) + List findActivePaidByRoomAndOverlap( + @Param("room") com.sumte.room.entity.Room room, + @Param("startInclusive") java.time.LocalDate startInclusive, + @Param("endExclusive") java.time.LocalDate endExclusive + ); + } diff --git a/src/main/java/com/sumte/room/controller/RoomController.java b/src/main/java/com/sumte/room/controller/RoomController.java index 21b3808..fb37a5a 100644 --- a/src/main/java/com/sumte/room/controller/RoomController.java +++ b/src/main/java/com/sumte/room/controller/RoomController.java @@ -150,4 +150,24 @@ public ApiResponse> getRoomsByGuesthouse( return ApiResponse.success(roomQueryService.getRoomsByGuesthouse(guesthouseId, startDate, endDate)); } + // 1) 게스트하우스: 앞으로 3개월간 모든 객실 매진일 + @GetMapping("/{guesthouseId}/fully-booked-dates") + @Operation(summary = "게스트하우스 3개월 매진일 조회", + description = "현재 날짜부터 3개월 동안 해당 게스트하우스의 모든 객실이 점유된 날짜 목록을 반환합니다.") + public ApiResponse> getFullyBookedDatesOfGuesthouse( + @PathVariable Long guesthouseId + ) { + return ApiResponse.success(roomQueryService.getFullyBookedDatesOfGuesthouse(guesthouseId)); + } + + // 2) 객실: 앞으로 3개월간 예약 불가일 + @GetMapping("/room/{roomId}/unavailable-dates") + @Operation(summary = "객실 3개월 예약 불가일 조회", + description = "현재 날짜부터 3개월 동안 해당 객실이 예약 불가한 날짜 목록을 반환합니다.") + public ApiResponse> getUnavailableDatesOfRoom( + @PathVariable Long roomId + ) { + return ApiResponse.success(roomQueryService.getUnavailableDatesOfRoom(roomId)); + } + } diff --git a/src/main/java/com/sumte/room/repository/RoomRepository.java b/src/main/java/com/sumte/room/repository/RoomRepository.java index bb6336e..efd7a86 100644 --- a/src/main/java/com/sumte/room/repository/RoomRepository.java +++ b/src/main/java/com/sumte/room/repository/RoomRepository.java @@ -25,4 +25,5 @@ public interface RoomRepository extends JpaRepository { //객실 조회 List findAllByGuesthouseId(Long guesthouseId); + long countByGuesthouseId(Long guesthouseId); } \ No newline at end of file diff --git a/src/main/java/com/sumte/room/service/RoomQueryService.java b/src/main/java/com/sumte/room/service/RoomQueryService.java index afbba98..b39f832 100644 --- a/src/main/java/com/sumte/room/service/RoomQueryService.java +++ b/src/main/java/com/sumte/room/service/RoomQueryService.java @@ -8,4 +8,6 @@ public interface RoomQueryService { List getRoomsByGuesthouse(Long guesthouseId, LocalDate startDate, LocalDate endDate); RoomResponseDTO.GetRoomResponse getRoomById(Long roomId); + List getFullyBookedDatesOfGuesthouse(Long guesthouseId); + List getUnavailableDatesOfRoom(Long roomId); } diff --git a/src/main/java/com/sumte/room/service/RoomQueryServiceImpl.java b/src/main/java/com/sumte/room/service/RoomQueryServiceImpl.java index 0bdc31f..a67aac8 100644 --- a/src/main/java/com/sumte/room/service/RoomQueryServiceImpl.java +++ b/src/main/java/com/sumte/room/service/RoomQueryServiceImpl.java @@ -1,11 +1,10 @@ package com.sumte.room.service; import java.time.LocalDate; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; +import com.sumte.reservation.entity.Reservation; import org.springframework.stereotype.Service; import com.sumte.apiPayload.code.error.CommonErrorCode; @@ -88,4 +87,59 @@ public List getRoomsByGuesthouse(Long guesthouseId, }) .collect(Collectors.toList()); } + + @Override + @Transactional + public List getFullyBookedDatesOfGuesthouse(Long guesthouseId) { + LocalDate start = LocalDate.now(); + LocalDate endExclusive = start.plusMonths(3); + + long totalRooms = roomRepository.countByGuesthouseId(guesthouseId); + if (totalRooms == 0) return List.of(); + + List reservations = + reservationRepository.findActivePaidByGuesthouseAndOverlap(guesthouseId, start, endExclusive); + + Map occupiedCountByDate = new HashMap<>(); + for (Reservation r : reservations) { + LocalDate s = r.getStartDate().isBefore(start) ? start : r.getStartDate(); + LocalDate e = r.getEndDate().isAfter(endExclusive) ? endExclusive : r.getEndDate(); + + for (LocalDate d = s; d.isBefore(e); d = d.plusDays(1)) { + occupiedCountByDate.merge(d, 1, Integer::sum); + } + } + + List fullyBooked = new ArrayList<>(); + for (LocalDate d = start; d.isBefore(endExclusive); d = d.plusDays(1)) { + if (occupiedCountByDate.getOrDefault(d, 0) >= totalRooms) { + fullyBooked.add(d); + } + } + return fullyBooked; + } + + @Override + @Transactional + public List getUnavailableDatesOfRoom(Long roomId) { + Room room = roomRepository.findById(roomId) + .orElseThrow(() -> new SumteException(CommonErrorCode.NOT_EXIST_ROOM)); + + LocalDate start = LocalDate.now(); + LocalDate endExclusive = start.plusMonths(3); + + List reservations = + reservationRepository.findActivePaidByRoomAndOverlap(room, start, endExclusive); + + Set unavailable = new LinkedHashSet<>(); + for (Reservation r : reservations) { + LocalDate s = r.getStartDate().isBefore(start) ? start : r.getStartDate(); + LocalDate e = r.getEndDate().isAfter(endExclusive) ? endExclusive : r.getEndDate(); + + for (LocalDate d = s; d.isBefore(e); d = d.plusDays(1)) { + unavailable.add(d); + } + } + return new ArrayList<>(unavailable); + } } From eb8bc8533b11e5ee70f42333889393316d9b8870 Mon Sep 17 00:00:00 2001 From: hyowon Date: Fri, 15 Aug 2025 02:12:10 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[feat/#73]=20=EA=B2=B0=EC=A0=9C=20=EC=95=88?= =?UTF-8?q?=EB=93=9C=EB=A1=9C=EC=9D=B4=EB=93=9C=20=EC=95=B1=20=EC=8A=A4?= =?UTF-8?q?=ED=82=A4=EB=A7=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/converter/PaymentConverter.java | 3 +- .../payment/dto/KakaoPayReadyResponseDTO.java | 1 + .../sumte/payment/dto/PaymentResponseDTO.java | 1 + .../payment/service/PaymentServiceImpl.java | 180 +++++++++--------- 4 files changed, 94 insertions(+), 91 deletions(-) diff --git a/src/main/java/com/sumte/payment/converter/PaymentConverter.java b/src/main/java/com/sumte/payment/converter/PaymentConverter.java index c34b366..d610725 100644 --- a/src/main/java/com/sumte/payment/converter/PaymentConverter.java +++ b/src/main/java/com/sumte/payment/converter/PaymentConverter.java @@ -22,10 +22,11 @@ public static Payment toEntity(PaymentRequestDTO.PaymentRequestCreate dto, Reser .build(); } - public static PaymentResponseDTO.PaymentReadyResponse toCreateResponse(Payment payment, String paymentUrl) { + public static PaymentResponseDTO.PaymentReadyResponse toCreateResponse(Payment payment, String paymentUrl, String appScheme) { return PaymentResponseDTO.PaymentReadyResponse.builder() .paymentId(payment.getId()) .paymentUrl(paymentUrl) + .appScheme(appScheme) .build(); } diff --git a/src/main/java/com/sumte/payment/dto/KakaoPayReadyResponseDTO.java b/src/main/java/com/sumte/payment/dto/KakaoPayReadyResponseDTO.java index c1806d9..9e70613 100644 --- a/src/main/java/com/sumte/payment/dto/KakaoPayReadyResponseDTO.java +++ b/src/main/java/com/sumte/payment/dto/KakaoPayReadyResponseDTO.java @@ -9,5 +9,6 @@ public class KakaoPayReadyResponseDTO { private String tid; private String next_redirect_app_url; private String next_redirect_pc_url; + private String android_app_scheme; private String created_at; } diff --git a/src/main/java/com/sumte/payment/dto/PaymentResponseDTO.java b/src/main/java/com/sumte/payment/dto/PaymentResponseDTO.java index cda9136..f2a7c1d 100644 --- a/src/main/java/com/sumte/payment/dto/PaymentResponseDTO.java +++ b/src/main/java/com/sumte/payment/dto/PaymentResponseDTO.java @@ -14,5 +14,6 @@ public class PaymentResponseDTO { public static class PaymentReadyResponse { private Long paymentId; private String paymentUrl; + private String appScheme; } } diff --git a/src/main/java/com/sumte/payment/service/PaymentServiceImpl.java b/src/main/java/com/sumte/payment/service/PaymentServiceImpl.java index 5a038b4..308b077 100644 --- a/src/main/java/com/sumte/payment/service/PaymentServiceImpl.java +++ b/src/main/java/com/sumte/payment/service/PaymentServiceImpl.java @@ -1,94 +1,94 @@ - package com.sumte.payment.service; - - import com.sumte.apiPayload.code.error.PaymentErrorCode; - import com.sumte.apiPayload.exception.SumteException; - import com.sumte.payment.converter.PaymentConverter; - import com.sumte.payment.dto.*; - import com.sumte.payment.entity.Payment; - import com.sumte.payment.entity.PaymentStatus; - import com.sumte.payment.kakaopay.KakaoPayClient; - import com.sumte.payment.repository.PaymentRepository; - import com.sumte.reservation.entity.Reservation; - import com.sumte.reservation.repository.ReservationRepository; - import lombok.RequiredArgsConstructor; - import org.springframework.beans.factory.annotation.Value; - import org.springframework.stereotype.Service; - import org.springframework.transaction.annotation.Transactional; - - @Service - @RequiredArgsConstructor - public class PaymentServiceImpl implements PaymentService { - - private final ReservationRepository reservationRepository; - private final PaymentRepository paymentRepository; - private final PaymentTransactionHelper transactionHelper; - private final KakaoPayClient kakaoPayClient; - - @Value("${kakao.pay.redirect.domain}") - private String redirectDomain; - - @Override - @Transactional - public PaymentResponseDTO.PaymentReadyResponse requestPayment(PaymentRequestDTO.PaymentRequestCreate dto) { - Reservation reservation = reservationRepository.findById(dto.getReservationId()) - .orElseThrow(() -> new SumteException(PaymentErrorCode.RESERVATION_NOT_FOUND)); - - Payment payment; - try { - payment = PaymentConverter.toEntity(dto, reservation); - } catch (IllegalArgumentException e) { - throw new SumteException(PaymentErrorCode.INVALID_PAYMENT_METHOD); - } - paymentRepository.save(payment); - - - String itemName = reservation.getRoom().getName(); - - KakaoPayReadyRequestDTO request = KakaoPayReadyRequestDTO.builder() - .cid("TC0ONETIME") - .partner_order_id("reservation_" + reservation.getId()) - .partner_user_id("user_" + reservation.getUser().getId()) - .item_name(itemName) - .quantity("1") - .total_amount(String.valueOf(dto.getAmount())) - .tax_free_amount("0") - .approval_url(redirectDomain + "/pay/success") - .cancel_url(redirectDomain + "/pay/cancel") - .fail_url(redirectDomain + "/pay/fail") - .build(); - - KakaoPayReadyResponseDTO kakaoResponse = kakaoPayClient.requestPayment(request); - payment.setTid(kakaoResponse.getTid()); - - return PaymentConverter.toCreateResponse(payment, kakaoResponse.getNext_redirect_app_url()); +package com.sumte.payment.service; + +import com.sumte.apiPayload.code.error.PaymentErrorCode; +import com.sumte.apiPayload.exception.SumteException; +import com.sumte.payment.converter.PaymentConverter; +import com.sumte.payment.dto.*; +import com.sumte.payment.entity.Payment; +import com.sumte.payment.entity.PaymentStatus; +import com.sumte.payment.kakaopay.KakaoPayClient; +import com.sumte.payment.repository.PaymentRepository; +import com.sumte.reservation.entity.Reservation; +import com.sumte.reservation.repository.ReservationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PaymentServiceImpl implements PaymentService { + + private final ReservationRepository reservationRepository; + private final PaymentRepository paymentRepository; + private final PaymentTransactionHelper transactionHelper; + private final KakaoPayClient kakaoPayClient; + + @Value("${kakao.pay.redirect.domain}") + private String redirectDomain; + + @Override + @Transactional + public PaymentResponseDTO.PaymentReadyResponse requestPayment(PaymentRequestDTO.PaymentRequestCreate dto) { + Reservation reservation = reservationRepository.findById(dto.getReservationId()) + .orElseThrow(() -> new SumteException(PaymentErrorCode.RESERVATION_NOT_FOUND)); + + Payment payment; + try { + payment = PaymentConverter.toEntity(dto, reservation); + } catch (IllegalArgumentException e) { + throw new SumteException(PaymentErrorCode.INVALID_PAYMENT_METHOD); } + paymentRepository.save(payment); - @Override - @Transactional - public KakaoPayApproveResponseDTO approvePayment(Long paymentId, String pgToken) { - Payment payment = paymentRepository.findById(paymentId) - .orElseThrow(() -> new SumteException(PaymentErrorCode.PAYMENT_NOT_FOUND)); - - if (payment.getPaymentStatus() == PaymentStatus.PAID) { - throw new SumteException(PaymentErrorCode.ALREADY_APPROVED_PAYMENT); - } - - if (pgToken == null || pgToken.isBlank()) { - transactionHelper.markPaymentFailed(payment); - throw new SumteException(PaymentErrorCode.PG_TOKEN_MISSING); - } - - KakaoPayApproveRequestDTO request = KakaoPayApproveRequestDTO.builder() - .cid("TC0ONETIME") - .tid(payment.getTid()) - .partner_order_id("reservation_" + payment.getReservation().getId()) - .partner_user_id("user_" + payment.getReservation().getUser().getId()) - .pg_token(pgToken) - .build(); - - KakaoPayApproveResponseDTO response = kakaoPayClient.approvePayment(request); - payment.markAsPaid(); - - return response; + + String itemName = reservation.getRoom().getName(); + + KakaoPayReadyRequestDTO request = KakaoPayReadyRequestDTO.builder() + .cid("TC0ONETIME") + .partner_order_id("reservation_" + reservation.getId()) + .partner_user_id("user_" + reservation.getUser().getId()) + .item_name(itemName) + .quantity("1") + .total_amount(String.valueOf(dto.getAmount())) + .tax_free_amount("0") + .approval_url(redirectDomain + "/pay/success") + .cancel_url(redirectDomain + "/pay/cancel") + .fail_url(redirectDomain + "/pay/fail") + .build(); + + KakaoPayReadyResponseDTO kakaoResponse = kakaoPayClient.requestPayment(request); + payment.setTid(kakaoResponse.getTid()); + + return PaymentConverter.toCreateResponse(payment, kakaoResponse.getNext_redirect_app_url(),kakaoResponse.getAndroid_app_scheme()); + } + + @Override + @Transactional + public KakaoPayApproveResponseDTO approvePayment(Long paymentId, String pgToken) { + Payment payment = paymentRepository.findById(paymentId) + .orElseThrow(() -> new SumteException(PaymentErrorCode.PAYMENT_NOT_FOUND)); + + if (payment.getPaymentStatus() == PaymentStatus.PAID) { + throw new SumteException(PaymentErrorCode.ALREADY_APPROVED_PAYMENT); + } + + if (pgToken == null || pgToken.isBlank()) { + transactionHelper.markPaymentFailed(payment); + throw new SumteException(PaymentErrorCode.PG_TOKEN_MISSING); } + + KakaoPayApproveRequestDTO request = KakaoPayApproveRequestDTO.builder() + .cid("TC0ONETIME") + .tid(payment.getTid()) + .partner_order_id("reservation_" + payment.getReservation().getId()) + .partner_user_id("user_" + payment.getReservation().getUser().getId()) + .pg_token(pgToken) + .build(); + + KakaoPayApproveResponseDTO response = kakaoPayClient.approvePayment(request); + payment.markAsPaid(); + + return response; } +} From 27c2b8acde02522711dd2f70b5c2653720ee8a73 Mon Sep 17 00:00:00 2001 From: hyowon Date: Fri, 15 Aug 2025 02:18:18 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[fix/#73]=20swagger=20=EC=83=81=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=99=98=EB=B6=88=20=EC=95=88=EB=9C=A8=EA=B2=8C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/sumte/payment/controller/RefundController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/sumte/payment/controller/RefundController.java b/src/main/java/com/sumte/payment/controller/RefundController.java index 626646f..9116fa0 100644 --- a/src/main/java/com/sumte/payment/controller/RefundController.java +++ b/src/main/java/com/sumte/payment/controller/RefundController.java @@ -1,5 +1,6 @@ package com.sumte.payment.controller; +import io.swagger.v3.oas.annotations.Hidden; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -17,6 +18,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +@Hidden @RestController @RequestMapping("/refunds") @RequiredArgsConstructor