diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/member/entity/BankType.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/member/entity/BankType.java new file mode 100644 index 0000000..50a86ca --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/member/entity/BankType.java @@ -0,0 +1,32 @@ +package Comprehensive_Design_Project.CUK_Compasser.domain.member.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum BankType { + + NH("농협은행"), + IBK("기업은행"), + MG("새마을금고"), + KB("국민은행"), + SC("SC제일은행"), + CU("신협"), + KAKAO("카카오뱅크"), + SHINHAN("신한은행"), + DGB("대구은행"), + TOSS("토스뱅크"), + HANA("하나은행"), + BUSAN("부산은행"), + WOORI("우리은행"), + GWANGJU("광주은행"), + JEONBUK("전북은행"), + POST("우체국"), + SUHYUP("수협은행"), + KDB("산업은행"), + CITI("씨티은행"), + K_BANK("케이뱅크"); + + private final String displayName; +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/member/entity/Member.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/member/entity/Member.java index 90f8930..3f78283 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/member/entity/Member.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/member/entity/Member.java @@ -56,6 +56,10 @@ public class Member extends BaseEntity { @Column(length = 20) private String phone; + @Enumerated(EnumType.STRING) + @Column(name = "bank_type", length = 30) + private BankType bankType; + /** 점장 승격 시 생성되는 1:1 */ @OneToOne(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private StoreManager storeManager; diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/member/service/MemberService.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/member/service/MemberService.java index 5cf9d84..e237b4f 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/member/service/MemberService.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/member/service/MemberService.java @@ -17,7 +17,6 @@ import com.google.zxing.common.BitMatrix; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/controller/OAuth2Controller.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/controller/OAuth2Controller.java index 9a74ace..53817c6 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/controller/OAuth2Controller.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/controller/OAuth2Controller.java @@ -74,7 +74,7 @@ public ApiResponse loginKakao(){ } @GetMapping("/code/kakao") // 카카오 회원가입 or 로그인 서비스 == GET 요청 - @Operation(summary = "카카오 로그인 API", description = "사용자가 카카오 로그인 동의 후 받는 API 입니다. 딱히 접근할 일이 없어요.") + @Operation(summary = "카카오 로그인 API", description = "사용자가 카카오 로그인 동의 후 받는 API 입니다. 카카오 서버에서 바로 BE 서버로 리다이렉트 해줘서 딱히 접근할 일이 없어요.") public ApiResponse callback (@RequestParam("code") String code, HttpServletResponse response) { MemberRespDTO.MemberInfoDTO memberInfo = oAuth2Service.loginWithKakao(code); @@ -100,6 +100,6 @@ public ApiResponse logout(HttpServletRequest request, @AuthenticationPri if (bearerToken != null && bearerToken.startsWith("Bearer ")) { oAuth2Service.logout(bearerToken.substring(7).trim(), userDetails.getMember().getId()); } - return null; // bool 값 리턴 + return ApiResponse.onSuccess(SuccessStatus.OK); // bool 값 리턴 } } diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/controller/OrderController.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/controller/OrderController.java new file mode 100644 index 0000000..aee8481 --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/controller/OrderController.java @@ -0,0 +1,62 @@ +package Comprehensive_Design_Project.CUK_Compasser.domain.order.controller; + +import Comprehensive_Design_Project.CUK_Compasser.domain.order.dto.OrderReqDTO; +import Comprehensive_Design_Project.CUK_Compasser.domain.order.dto.OrderRespDTO; +import Comprehensive_Design_Project.CUK_Compasser.domain.order.service.OrderService; +import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.ApiResponse; +import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.code.status.SuccessStatus; +import Comprehensive_Design_Project.CUK_Compasser.global.security.userDetails.CustomUserDetails; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/orders") +public class OrderController { + + private final OrderService orderService; + + /** + * 사용자 주문 생성 API + */ + @PostMapping + public ApiResponse createOrder( + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestBody @Valid OrderReqDTO.CreateOrderDTO request + ) { + return ApiResponse.onSuccess( + SuccessStatus.ORDER_CREATED, + orderService.createOrder(userDetails.getMember().getId(), request) + ); + } + + /** + * 사용자 주문 취소 API + */ + @PatchMapping("/{orderId}/cancel") + public ApiResponse cancelOrder( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long orderId + ) { + return ApiResponse.onSuccess( + SuccessStatus.ORDER_CANCELED, + orderService.cancelOrder(userDetails.getMember().getId(), orderId) + ); + } + + /** + * 사용자 주문 상태 조회 API + */ + @GetMapping("/{orderId}/status") + public ApiResponse getOrderStatus( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long orderId + ) { + return ApiResponse.onSuccess( + SuccessStatus.ORDER_STATUS_FOUND, + orderService.getOrderStatus(userDetails.getMember().getId(), orderId) + ); + } +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/converter/OrderConverter.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/converter/OrderConverter.java new file mode 100644 index 0000000..46fad98 --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/converter/OrderConverter.java @@ -0,0 +1,83 @@ +package Comprehensive_Design_Project.CUK_Compasser.domain.order.converter; + +import Comprehensive_Design_Project.CUK_Compasser.domain.order.dto.OrderRespDTO; +import Comprehensive_Design_Project.CUK_Compasser.domain.order.entity.OrderStatus; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity.Reservation; + +public class OrderConverter { + + private OrderConverter() { + } + + public static OrderRespDTO.CreateOrderResultDTO toCreateOrderResultDTO(Reservation reservation) { + OrderStatus orderStatus = OrderStatusMapper.resolve(reservation); + + return OrderRespDTO.CreateOrderResultDTO.builder() + .reservationId(reservation.getId()) + .storeId(reservation.getStore().getId()) + .storeName(reservation.getStore().getStoreName()) + .randomBoxId(reservation.getRandomBox().getId()) + .randomBoxName(reservation.getRandomBox().getBoxName()) + .quantity(reservation.getRequestedQuantity()) + .unitPrice(reservation.getRandomBox().getPrice()) + .totalPrice(reservation.getTotalPrice()) + .orderStatus(orderStatus.name()) + .reservationStatus(reservation.getStatus().name()) + .paymentStatus(reservation.getPaymentStatus() != null ? reservation.getPaymentStatus().name() : null) + .pickupStatus(reservation.getPickupStatus() != null ? reservation.getPickupStatus().name() : null) + .memberBankType(reservation.getMember().getBankType()) + .depositBankType(reservation.getStore().getStoreManager().getDepositBankType()) + .depositAccountNumber(reservation.getStore().getStoreManager().getDepositAccountNumber()) + .depositAccountHolder(reservation.getStore().getStoreManager().getDepositAccountHolder()) + .businessHours(reservation.getStore().getBusinessHours()) + .createdAt(reservation.getCreatedAt()) + .build(); + } + + public static OrderRespDTO.CompleteOrderResultDTO toCompleteOrderResultDTO(Reservation reservation, String message) { + return OrderRespDTO.CompleteOrderResultDTO.builder() + .reservationId(reservation.getId()) + .orderStatus(OrderStatusMapper.resolve(reservation).name()) + .reservationStatus(reservation.getStatus().name()) + .paymentStatus(reservation.getPaymentStatus() != null ? reservation.getPaymentStatus().name() : null) + .pickupStatus(reservation.getPickupStatus() != null ? reservation.getPickupStatus().name() : null) + .message(message) + .build(); + } + + public static OrderRespDTO.OrderStatusDTO toOrderStatusDTO(Reservation reservation) { + OrderStatus orderStatus = OrderStatusMapper.resolve(reservation); + + return OrderRespDTO.OrderStatusDTO.builder() + .reservationId(reservation.getId()) + .storeId(reservation.getStore().getId()) + .storeName(reservation.getStore().getStoreName()) + .randomBoxId(reservation.getRandomBox().getId()) + .randomBoxName(reservation.getRandomBox().getBoxName()) + .quantity(reservation.getRequestedQuantity()) + .totalPrice(reservation.getTotalPrice()) + .orderStatus(orderStatus.name()) + .reservationStatus(reservation.getStatus().name()) + .paymentStatus(reservation.getPaymentStatus() != null ? reservation.getPaymentStatus().name() : null) + .pickupStatus(reservation.getPickupStatus() != null ? reservation.getPickupStatus().name() : null) + .memberBankType(reservation.getMember().getBankType()) + .depositBankType(reservation.getStore().getStoreManager().getDepositBankType()) + .depositAccountNumber(reservation.getStore().getStoreManager().getDepositAccountNumber()) + .depositAccountHolder(reservation.getStore().getStoreManager().getDepositAccountHolder()) + .businessHours(reservation.getStore().getBusinessHours()) + .createdAt(reservation.getCreatedAt()) + .updatedAt(reservation.getUpdatedAt()) + .build(); + } + + public static OrderRespDTO.CancelOrderResultDTO toCancelOrderResultDTO(Reservation reservation, String message) { + return OrderRespDTO.CancelOrderResultDTO.builder() + .reservationId(reservation.getId()) + .orderStatus(OrderStatusMapper.resolve(reservation).name()) + .reservationStatus(reservation.getStatus().name()) + .paymentStatus(reservation.getPaymentStatus() != null ? reservation.getPaymentStatus().name() : null) + .pickupStatus(reservation.getPickupStatus() != null ? reservation.getPickupStatus().name() : null) + .message(message) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/converter/OrderStatusMapper.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/converter/OrderStatusMapper.java new file mode 100644 index 0000000..cc1d2dc --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/converter/OrderStatusMapper.java @@ -0,0 +1,70 @@ +package Comprehensive_Design_Project.CUK_Compasser.domain.order.converter; + +import Comprehensive_Design_Project.CUK_Compasser.domain.order.entity.OrderStatus; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity.PaymentStatus; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity.PickupStatus; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity.Reservation; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity.ReservationStatus; + +public class OrderStatusMapper { + + private OrderStatusMapper() { + } + + /** + * Reservation의 여러 상태값을 조합해서 + * 사용자 화면에서 보여줄 대표 주문 상태(OrderStatus)를 계산한다. + * + * 우선순위: + * 1. 취소/거절 + * 2. 픽업 완료 + * 3. 픽업 준비 완료 + * 4. 준비중 + * 5. 결제 완료 + * 6. 점주 승인 + * 7. 기본 생성 상태 + */ + /** + * 사용자 주문 화면에 표시할 대표 상태를 계산하는 클래스 + * + * 실제 상태 원천은 ReservationStatus, PaymentStatus, PickupStatus이며, + * OrderStatus는 다른 코드와의 호환을 위해 대표 상태로만 사용한다. + */ + public static OrderStatus resolve(Reservation reservation) { + if (reservation == null) { + return OrderStatus.CREATED; + } + + ReservationStatus reservationStatus = reservation.getStatus(); + PaymentStatus paymentStatus = reservation.getPaymentStatus(); + PickupStatus pickupStatus = reservation.getPickupStatus(); + + // 사용자가 취소했거나 점주가 거절한 경우 + if (reservationStatus == ReservationStatus.CANCELED || + reservationStatus == ReservationStatus.REJECTED) { + return OrderStatus.CANCELED; + } + + // 픽업 상태 우선 반영 + if (pickupStatus == PickupStatus.PICKED_UP) { + return OrderStatus.PICKED_UP; + } + + if (pickupStatus == PickupStatus.READY) { + return OrderStatus.READY; + } + + if (pickupStatus == PickupStatus.PREPARING) { + return OrderStatus.PREPARING; + } + + // 결제 완료 + if (paymentStatus == PaymentStatus.PAID) { + return OrderStatus.PAID; + } + + // 승인만 된 상태는 아직 사용자 화면에서는 CREATED 또는 PAID 이전 단계로 볼 수 있음 + // 정책상 별도 APPROVED가 없으므로 CREATED로 흡수 + return OrderStatus.CREATED; + } +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/dto/OrderReqDTO.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/dto/OrderReqDTO.java new file mode 100644 index 0000000..c885294 --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/dto/OrderReqDTO.java @@ -0,0 +1,41 @@ +package Comprehensive_Design_Project.CUK_Compasser.domain.order.dto; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class OrderReqDTO { + + @Getter + public static class CreateOrderDTO { + + @NotNull + private Long randomBoxId; + + @NotNull + @Min(1) + private Integer quantity; + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class CancelOrderResultDTO { + private Long reservationId; + + // 사용자 화면에 보여줄 대표 주문 상태 + private String orderStatus; + + // 실제 DB 상태값 + private String reservationStatus; + private String paymentStatus; + private String pickupStatus; + + private String message; + } + +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/dto/OrderRespDTO.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/dto/OrderRespDTO.java new file mode 100644 index 0000000..5508a73 --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/dto/OrderRespDTO.java @@ -0,0 +1,100 @@ +package Comprehensive_Design_Project.CUK_Compasser.domain.order.dto; + +import Comprehensive_Design_Project.CUK_Compasser.domain.member.entity.BankType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +public class OrderRespDTO { + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class CreateOrderResultDTO { + private Long reservationId; + private Long storeId; + private String storeName; + + private Long randomBoxId; + private String randomBoxName; + + private Integer quantity; + private Integer unitPrice; + private Integer totalPrice; + + private String orderStatus; + private String reservationStatus; + private String paymentStatus; + private String pickupStatus; + + private BankType memberBankType; + + private BankType depositBankType; + private String depositAccountNumber; + private String depositAccountHolder; + + private String businessHours; + private LocalDateTime createdAt; + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class CompleteOrderResultDTO { + private Long reservationId; + private String orderStatus; + private String reservationStatus; + private String paymentStatus; + private String pickupStatus; + private String message; + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class CancelOrderResultDTO { + private Long reservationId; + private String orderStatus; + private String reservationStatus; + private String paymentStatus; + private String pickupStatus; + private String message; + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class OrderStatusDTO { + private Long reservationId; + private Long storeId; + private String storeName; + + private Long randomBoxId; + private String randomBoxName; + + private Integer quantity; + private Integer totalPrice; + + private String orderStatus; + private String reservationStatus; + private String paymentStatus; + private String pickupStatus; + + private BankType memberBankType; + + private BankType depositBankType; + private String depositAccountNumber; + private String depositAccountHolder; + + private String businessHours; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + } +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/service/OrderService.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/service/OrderService.java new file mode 100644 index 0000000..6dc3867 --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/service/OrderService.java @@ -0,0 +1,13 @@ +package Comprehensive_Design_Project.CUK_Compasser.domain.order.service; + +import Comprehensive_Design_Project.CUK_Compasser.domain.order.dto.OrderReqDTO; +import Comprehensive_Design_Project.CUK_Compasser.domain.order.dto.OrderRespDTO; + +public interface OrderService { + + OrderRespDTO.CreateOrderResultDTO createOrder(Long memberId, OrderReqDTO.CreateOrderDTO request); + + OrderRespDTO.CancelOrderResultDTO cancelOrder(Long memberId, Long orderId); + + OrderRespDTO.OrderStatusDTO getOrderStatus(Long memberId, Long orderId); +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/service/OrderServiceImpl.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/service/OrderServiceImpl.java new file mode 100644 index 0000000..da0019d --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/order/service/OrderServiceImpl.java @@ -0,0 +1,141 @@ +package Comprehensive_Design_Project.CUK_Compasser.domain.order.service; + +import Comprehensive_Design_Project.CUK_Compasser.domain.member.entity.Member; +import Comprehensive_Design_Project.CUK_Compasser.domain.member.repository.MemberRepository; +import Comprehensive_Design_Project.CUK_Compasser.domain.order.converter.OrderConverter; +import Comprehensive_Design_Project.CUK_Compasser.domain.order.dto.OrderReqDTO; +import Comprehensive_Design_Project.CUK_Compasser.domain.order.dto.OrderRespDTO; +import Comprehensive_Design_Project.CUK_Compasser.domain.randomBox.entity.RandomBox; +import Comprehensive_Design_Project.CUK_Compasser.domain.randomBox.entity.SaleStatus; +import Comprehensive_Design_Project.CUK_Compasser.domain.randomBox.repository.RandomBoxRepository; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity.PaymentStatus; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity.PickupStatus; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity.Reservation; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity.ReservationStatus; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.repository.ReservationRepository; +import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.code.status.ErrorStatus; +import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.exception.GeneralException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class OrderServiceImpl implements OrderService { + + private final MemberRepository memberRepository; + private final RandomBoxRepository randomBoxRepository; + private final ReservationRepository reservationRepository; + + /** + * 사용자 주문 생성 + * - 사용자 주문 화면에서 사용할 Reservation 데이터를 생성한다. + * - 주문 생성 시 예약/결제/픽업 상태의 기본값을 함께 세팅한다. + * - 결제는 이후 카카오페이 ready/approve API에서 진행한다. + */ + @Override + @Transactional + public OrderRespDTO.CreateOrderResultDTO createOrder(Long memberId, OrderReqDTO.CreateOrderDTO request) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND)); + + RandomBox randomBox = randomBoxRepository.findById(request.getRandomBoxId()) + .orElseThrow(() -> new GeneralException(ErrorStatus.RANDOM_BOX_NOT_FOUND)); + + validateCreateOrder(randomBox, request.getQuantity()); + + int totalPrice = randomBox.getPrice() * request.getQuantity(); + + Reservation reservation = Reservation.builder() + .member(member) + .store(randomBox.getStore()) + .randomBox(randomBox) + .requestedQuantity(request.getQuantity()) + .totalPrice(totalPrice) + .status(ReservationStatus.REQUESTED) + .paymentStatus(PaymentStatus.PENDING) + .pickupStatus(PickupStatus.WAITING) + .build(); + + Reservation savedReservation = reservationRepository.save(reservation); + + return OrderConverter.toCreateOrderResultDTO(savedReservation); + } + + /** + * 사용자 주문 취소 + * - 아직 점주가 처리하지 않은 REQUESTED 상태의 주문만 취소할 수 있다. + * - 이미 결제 완료된 주문은 취소할 수 없다. + */ + @Override + @Transactional + public OrderRespDTO.CancelOrderResultDTO cancelOrder(Long memberId, Long orderId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND)); + + Reservation reservation = reservationRepository.findByIdAndMember(orderId, member) + .orElseThrow(() -> new GeneralException(ErrorStatus.RESERVATION_NOT_FOUND)); + + validateCancelOrder(reservation); + + reservation.setStatus(ReservationStatus.CANCELED); + reservation.setPaymentStatus(PaymentStatus.CANCELED); + + return OrderConverter.toCancelOrderResultDTO( + reservation, + "주문이 취소되었습니다." + ); + } + + /** + * 사용자 주문 상태 조회 + * - 사용자 화면에서 보여줄 주문 상태 상세 정보를 반환한다. + */ + @Override + public OrderRespDTO.OrderStatusDTO getOrderStatus(Long memberId, Long orderId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND)); + + Reservation reservation = reservationRepository.findByIdAndMember(orderId, member) + .orElseThrow(() -> new GeneralException(ErrorStatus.RESERVATION_NOT_FOUND)); + + return OrderConverter.toOrderStatusDTO(reservation); + } + + /** + * 주문 생성 가능 여부 검증 + */ + private void validateCreateOrder(RandomBox randomBox, Integer quantity) { + if (quantity == null || quantity < 1) { + throw new GeneralException(ErrorStatus.INVALID_ORDER_QUANTITY); + } + + if (randomBox.getSaleStatus() != SaleStatus.READY) { + throw new GeneralException(ErrorStatus.RANDOM_BOX_NOT_ON_SALE); + } + + if (randomBox.getStock() < quantity) { + throw new GeneralException(ErrorStatus.RANDOM_BOX_STOCK_NOT_ENOUGH); + } + + if (randomBox.getBuyLimit() != null && quantity > randomBox.getBuyLimit()) { + throw new GeneralException(ErrorStatus.RANDOM_BOX_BUY_LIMIT_EXCEEDED); + } + } + + /** + * 주문 취소 가능 여부 검증 + * - 아직 점주가 처리하지 않은 REQUESTED 상태만 취소 가능하다. + * - 결제 완료된 예약은 취소 불가 + */ + private void validateCancelOrder(Reservation reservation) { + if (reservation.getStatus() != ReservationStatus.REQUESTED) { + throw new GeneralException(ErrorStatus.INVALID_RESERVATION_STATUS); + } + + if (reservation.getPaymentStatus() == PaymentStatus.PAID) { + throw new GeneralException(ErrorStatus.ORDER_ALREADY_PAID); + } + } +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/randomBox/entity/RandomBox.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/randomBox/entity/RandomBox.java index ce33ebf..6d034c0 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/randomBox/entity/RandomBox.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/randomBox/entity/RandomBox.java @@ -2,6 +2,8 @@ import Comprehensive_Design_Project.CUK_Compasser.domain.store.entity.Store; import Comprehensive_Design_Project.CUK_Compasser.global.common.BaseEntity; +import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.code.status.ErrorStatus; +import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.exception.GeneralException; import jakarta.persistence.*; import lombok.*; @@ -50,4 +52,36 @@ public class RandomBox extends BaseEntity { @Enumerated(EnumType.STRING) @Column(name = "sale_status", nullable = false, length = 30) private SaleStatus saleStatus; + + public void decreaseStock(int quantity) { + if (quantity <= 0) { + throw new GeneralException(ErrorStatus.INVALID_ORDER_QUANTITY); + } + + if (this.saleStatus != SaleStatus.READY) { + throw new GeneralException(ErrorStatus.RANDOM_BOX_NOT_ON_SALE); + } + + if (this.stock < quantity) { + throw new GeneralException(ErrorStatus.RANDOM_BOX_STOCK_NOT_ENOUGH); + } + + this.stock -= quantity; + + if (this.stock == 0) { + this.saleStatus = SaleStatus.SOLD_OUT; + } + } + + public void increaseStock(int quantity) { + if (quantity <= 0) { + return; + } + + this.stock += quantity; + + if (this.stock > 0 && this.saleStatus == SaleStatus.SOLD_OUT) { + this.saleStatus = SaleStatus.READY; + } + } } \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/randomBox/entity/SaleStatus.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/randomBox/entity/SaleStatus.java index 396bca8..7733e0c 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/randomBox/entity/SaleStatus.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/randomBox/entity/SaleStatus.java @@ -1,5 +1,10 @@ package Comprehensive_Design_Project.CUK_Compasser.domain.randomBox.entity; public enum SaleStatus { - SOLD_OUT, READY, PICKED_UP, CANCELED + //랜덤박스 판매 가능 상태 설정 + /** + * 랜덤박스 판매 가능 상태 + * 상품 등록/조회/주문 가능 여부 검증에 사용 + */ + SOLD_OUT, READY, CANCELED } \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/randomBox/repository/RandomBoxRepository.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/randomBox/repository/RandomBoxRepository.java index 3800f03..9ab2199 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/randomBox/repository/RandomBoxRepository.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/randomBox/repository/RandomBoxRepository.java @@ -1,7 +1,9 @@ package Comprehensive_Design_Project.CUK_Compasser.domain.randomBox.repository; import Comprehensive_Design_Project.CUK_Compasser.domain.randomBox.entity.RandomBox; +import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; import java.util.List; import java.util.Optional; @@ -9,4 +11,8 @@ public interface RandomBoxRepository extends JpaRepository { List findAllByStore_Id(Long storeId); Optional findByIdAndStore_Id(Long boxId, Long storeId); + + //트랜젝션 락킹을 위한 락 조회 메서드 + @Lock(LockModeType.PESSIMISTIC_WRITE) + Optional findWithLockById(Long id); } \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/controller/KakaoPayCallbackController.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/controller/KakaoPayCallbackController.java new file mode 100644 index 0000000..1cf6d40 --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/controller/KakaoPayCallbackController.java @@ -0,0 +1,50 @@ +package Comprehensive_Design_Project.CUK_Compasser.domain.reservation.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/payments/kakaopay") +public class KakaoPayCallbackController { + + @GetMapping("/success") + public ResponseEntity success( + @RequestParam Long reservationId, + @RequestParam("pg_token") String pgToken + ) { + URI redirectUri = URI.create( + "http://localhost:3000/payment/success?reservationId=" + reservationId + "&pg_token=" + pgToken + ); + + return ResponseEntity.status(302) + .header(HttpHeaders.LOCATION, redirectUri.toString()) + .build(); + } + + @GetMapping("/cancel") + public ResponseEntity cancel(@RequestParam Long reservationId) { + URI redirectUri = URI.create( + "http://localhost:3000/payment/cancel?reservationId=" + reservationId + ); + + return ResponseEntity.status(302) + .header(HttpHeaders.LOCATION, redirectUri.toString()) + .build(); + } + + @GetMapping("/fail") + public ResponseEntity fail(@RequestParam Long reservationId) { + URI redirectUri = URI.create( + "http://localhost:3000/payment/fail?reservationId=" + reservationId + ); + + return ResponseEntity.status(302) + .header(HttpHeaders.LOCATION, redirectUri.toString()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/controller/ReservationPaymentController.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/controller/ReservationPaymentController.java new file mode 100644 index 0000000..d1fd322 --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/controller/ReservationPaymentController.java @@ -0,0 +1,76 @@ +package Comprehensive_Design_Project.CUK_Compasser.domain.reservation.controller; + +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.dto.KakaoPayRespDTO; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.service.ReservationService; +import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.ApiResponse; +import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.code.status.SuccessStatus; +import Comprehensive_Design_Project.CUK_Compasser.global.security.userDetails.CustomUserDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/reservations") +@Tag(name = "Reservation Payment", description = "사용자 예약 결제 API") +public class ReservationPaymentController { + + private final ReservationService reservationService; + + @Operation( + summary = "카카오페이 결제 준비", + description = "사용자 본인 예약 기준으로 카카오페이 결제창 진입을 위한 tid와 redirectUrl을 생성합니다." + ) + @PostMapping("/{reservationId}/payment/ready") + public ApiResponse readyKakaoPay( + @PathVariable Long reservationId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + return ApiResponse.onSuccess( + SuccessStatus.RESERVATION_PAYMENT_READY, + reservationService.readyKakaoPay(reservationId, userDetails.getMember().getId()) + ); + } + + @Operation( + summary = "카카오페이 결제 승인", + description = "카카오페이 결제 완료 후 전달받은 pg_token으로 최종 결제를 승인합니다." + ) + @PostMapping("/{reservationId}/payment/approve") + public ApiResponse approveKakaoPay( + @PathVariable Long reservationId, + @RequestParam("pg_token") String pgToken, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + return ApiResponse.onSuccess( + SuccessStatus.RESERVATION_PAYMENT_APPROVED, + reservationService.approveKakaoPay(reservationId, userDetails.getMember().getId(), pgToken) + ); + } + + /* + 결제 취소 API + 이건 주문은 살려두고 결제만 취소하는 것이야. + reservationStatus = REQUESTED 그대로 + paymentStatus = CANCELED + + 예약은 아직 유효 + 다만 이번 결제 시도만 취소됨 + 사용자는 같은 reservationId로 다시 결제 가능 + */ + @Operation( + summary = "카카오페이 결제전 결제창에 취소 처리", + description = "결제창에서 사용자가 취소한 경우 결제 상태만 CANCELED로 변경합니다." + ) + @PostMapping("/{reservationId}/payment/cancel") + public ApiResponse cancelKakaoPay( + @PathVariable Long reservationId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + reservationService.cancelKakaoPay(reservationId, userDetails.getMember().getId()); + return ApiResponse.onSuccess(SuccessStatus.OK, null); + } + +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/dto/KakaoPayReqDTO.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/dto/KakaoPayReqDTO.java new file mode 100644 index 0000000..6fd742e --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/dto/KakaoPayReqDTO.java @@ -0,0 +1,35 @@ +package Comprehensive_Design_Project.CUK_Compasser.domain.reservation.dto; + +import lombok.*; + +public class KakaoPayReqDTO { + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ReadyRequestDTO { + private String cid; + private String partner_order_id; + private String partner_user_id; + private String item_name; + private Integer quantity; + private Integer total_amount; + private Integer tax_free_amount; + private String approval_url; + private String cancel_url; + private String fail_url; + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ApproveRequestDTO { + private String cid; + private String tid; + private String partner_order_id; + private String partner_user_id; + private String pg_token; + } +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/dto/KakaoPayRespDTO.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/dto/KakaoPayRespDTO.java new file mode 100644 index 0000000..daa850a --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/dto/KakaoPayRespDTO.java @@ -0,0 +1,67 @@ +package Comprehensive_Design_Project.CUK_Compasser.domain.reservation.dto; + +import lombok.*; + +public class KakaoPayRespDTO { + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class ReadyResponseDTO { + private String tid; + private String next_redirect_app_url; + private String next_redirect_mobile_url; + private String next_redirect_pc_url; + private String created_at; + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class ApproveResponseDTO { + private String aid; + private String tid; + private String cid; + private String partner_order_id; + private String partner_user_id; + private String item_name; + private Integer quantity; + private Amount amount; + private String approved_at; + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class Amount { + private Integer total; + private Integer tax_free; + private Integer vat; + private Integer discount; + } + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ReadyResultDTO { + private Long reservationId; + private String tid; + private String redirectUrl; + private String paymentStatus; + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ApproveResultDTO { + private Long reservationId; + private String paymentMethod; + private String paymentStatus; + private String approvedAt; + } +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/entity/PaymentStatus.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/entity/PaymentStatus.java new file mode 100644 index 0000000..146e6a8 --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/entity/PaymentStatus.java @@ -0,0 +1,9 @@ +package Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity; + +public enum PaymentStatus { + PENDING, // 결제 전 + READY, // 카카오페이 ready 완료, 결제창 진입 가능 + PAID, // 승인 완료 + FAILED, // 결제 실패 + CANCELED // 결제 취소 +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/entity/PickupStatus.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/entity/PickupStatus.java new file mode 100644 index 0000000..7662325 --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/entity/PickupStatus.java @@ -0,0 +1,13 @@ +package Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity; + +public enum PickupStatus { + //픽업 수령/준비 흐 상태 + /** + * 사장님 상품 준비 및 픽업 상태 + * 점주 운영 API에서 사용 + */ + WAITING, + PREPARING, + READY, + PICKED_UP +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/entity/Reservation.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/entity/Reservation.java index 0db1267..06f4f57 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/entity/Reservation.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/entity/Reservation.java @@ -7,6 +7,8 @@ import jakarta.persistence.*; import lombok.*; +import java.time.LocalDateTime; + @Entity @Table( name = "reservations", @@ -31,32 +33,23 @@ public class Reservation extends BaseEntity { // 예약한 사용자 @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn( - name = "member_id", - nullable = false, - foreignKey = @ForeignKey(name = "fk_resv_member") - ) + @JoinColumn(name = "member_id", nullable = false, + foreignKey = @ForeignKey(name = "fk_resv_member")) private Member member; // 예약이 속한 스토어 - // 일단 기존 구조 호환 때문에 유지 @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn( - name = "store_id", - nullable = false, - foreignKey = @ForeignKey(name = "fk_resv_store") - ) + @JoinColumn(name = "store_id", nullable = false, + foreignKey = @ForeignKey(name = "fk_resv_store")) private Store store; // 예약 대상 랜덤박스 @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn( - name = "random_box_id", - nullable = false, - foreignKey = @ForeignKey(name = "fk_resv_random_box") - ) + @JoinColumn(name = "random_box_id", nullable = false, + foreignKey = @ForeignKey(name = "fk_resv_random_box")) private RandomBox randomBox; + // 예약 상태: 요청/승인/거절/취소 @Builder.Default @Enumerated(EnumType.STRING) @Column(name = "status", nullable = false, length = 30) @@ -66,14 +59,108 @@ public class Reservation extends BaseEntity { @Column(name = "requested_quantity", nullable = false) private Integer requestedQuantity; - // 거절 사유 + // 총 결제 금액 + @Column(name = "total_price", nullable = false) + private Integer totalPrice; + + // 픽업 완료 시각 + @Column(name = "picked_up_at") + private LocalDateTime pickedUpAt; + + // 결제 상태 + @Builder.Default + @Enumerated(EnumType.STRING) + @Column(name = "payment_status", nullable = false, length = 30) + private PaymentStatus paymentStatus = PaymentStatus.PENDING; + + // 픽업 상태 + @Builder.Default + @Enumerated(EnumType.STRING) + @Column(name = "pickup_status", nullable = false, length = 30) + private PickupStatus pickupStatus = PickupStatus.WAITING; + + // 거절/취소 사유 @Column(name = "reject_reason", length = 500) private String rejectReason; + // 카카오페이 ready 응답의 tid + @Column(name = "payment_tid", length = 100) + private String paymentTid; + + // 결제 수단 + @Column(name = "payment_method", length = 30) + private String paymentMethod; + + // 실제 결제 완료 시각 + @Column(name = "paid_at") + private LocalDateTime paidAt; + @PrePersist protected void prePersist() { if (this.status == null) { this.status = ReservationStatus.REQUESTED; } + if (this.paymentStatus == null) { + this.paymentStatus = PaymentStatus.PENDING; + } + if (this.pickupStatus == null) { + this.pickupStatus = PickupStatus.WAITING; + } + } + + /** + * 카카오페이 결제 준비 완료 + * - ready API 응답으로 받은 tid 저장 + * - 결제 진행 가능 상태로 변경 + */ + public void markPaymentReady(String paymentTid, String paymentMethod) { + this.paymentTid = paymentTid; + this.paymentMethod = paymentMethod; + this.paymentStatus = PaymentStatus.READY; + } + + /** + * 카카오페이 결제 승인 완료 + */ + public void markPaid(String paymentMethod) { + this.paymentMethod = paymentMethod; + this.paymentStatus = PaymentStatus.PAID; + this.paidAt = LocalDateTime.now(); + } + + /** + * 결제 실패 처리 + * - 예약 자체는 유지하고 결제 상태만 FAILED 처리 + */ + public void markPaymentFailed() { + this.paymentStatus = PaymentStatus.FAILED; + } + + /** + * 결제 취소 처리 + * - 예약 자체는 유지하고 결제 상태만 CANCELED 처리 + */ + public void cancelPayment() { + this.paymentStatus = PaymentStatus.CANCELED; + } + + public void approve() { + this.status = ReservationStatus.APPROVED; + this.rejectReason = null; + } + + public void reject(String rejectReason) { + this.status = ReservationStatus.REJECTED; + this.rejectReason = rejectReason; + this.pickupStatus = PickupStatus.WAITING; + } + + public void markPreparing() { + this.pickupStatus = PickupStatus.PREPARING; + } + + public void markPickedUp() { + this.pickupStatus = PickupStatus.PICKED_UP; + this.pickedUpAt = LocalDateTime.now(); } } \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/entity/ReservationStatus.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/entity/ReservationStatus.java index 5baa483..db3ef32 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/entity/ReservationStatus.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/entity/ReservationStatus.java @@ -1,5 +1,10 @@ package Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity; public enum ReservationStatus { + /** + * 주문 요청에 대한 승인/거절/취소 상태 + * 사장님 승인/거절 처리와 사용자 취소 처리에 사용 + */ + //점주가 예약 요청을 처리하는 승인 흐름 REQUESTED, APPROVED, REJECTED, CANCELED } \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/repository/ReservationRepository.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/repository/ReservationRepository.java index 8e47ffc..2c23927 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/repository/ReservationRepository.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/repository/ReservationRepository.java @@ -1,5 +1,7 @@ package Comprehensive_Design_Project.CUK_Compasser.domain.reservation.repository; +import Comprehensive_Design_Project.CUK_Compasser.domain.member.entity.Member; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity.PickupStatus; import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity.Reservation; import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity.ReservationStatus; import org.springframework.data.jpa.repository.EntityGraph; @@ -21,4 +23,29 @@ public interface ReservationRepository extends JpaRepository @EntityGraph(attributePaths = {"member", "store", "randomBox"}) Optional findById(Long reservationId); + + ///////////// 오더에 사용하는 레포 + + @EntityGraph(attributePaths = {"store", "randomBox"}) + List findAllByMemberOrderByCreatedAtDesc(Member member); + + @EntityGraph(attributePaths = {"store", "randomBox"}) + List findAllByMemberAndPickupStatusNotOrderByCreatedAtDesc(Member member, PickupStatus pickupStatus); + + @EntityGraph(attributePaths = {"store", "randomBox"}) + List findAllByMemberAndPickupStatusOrderByCreatedAtDesc(Member member, PickupStatus pickupStatus); + + boolean existsByRandomBox_IdAndMember_IdAndStatusIn(Long randomBoxId, Long memberId, List statuses); + + Optional findByIdAndMember_Id(Long reservationId, Long memberId); + + /// //////// + @EntityGraph(attributePaths = { + "member", + "store", + "store.storeManager", + "randomBox" + }) + Optional findByIdAndMember(Long reservationId, Member member); + } \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/service/KakaoPayClient.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/service/KakaoPayClient.java new file mode 100644 index 0000000..445ff81 --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/service/KakaoPayClient.java @@ -0,0 +1,69 @@ +package Comprehensive_Design_Project.CUK_Compasser.domain.reservation.service; + +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.dto.KakaoPayReqDTO; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.dto.KakaoPayRespDTO; +import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.code.status.ErrorStatus; +import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.exception.GeneralException; +import Comprehensive_Design_Project.CUK_Compasser.global.config.KakaoPayProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +@Slf4j +@Component +@RequiredArgsConstructor +public class KakaoPayClient { + + private final WebClient kakaoPayWebClient; + private final KakaoPayProperties kakaoPayProperties; + + public KakaoPayRespDTO.ReadyResponseDTO ready(KakaoPayReqDTO.ReadyRequestDTO request) { + try { + log.info("[KakaoPayClient] ready 호출 시작 - url={}", + kakaoPayProperties.getBaseUrl() + "/online/v1/payment/ready"); + + KakaoPayRespDTO.ReadyResponseDTO response = kakaoPayWebClient.post() + .uri(kakaoPayProperties.getBaseUrl() + "/online/v1/payment/ready") + .header(HttpHeaders.AUTHORIZATION, "SECRET_KEY " + kakaoPayProperties.getSecretKey()) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(request) + .retrieve() + .bodyToMono(KakaoPayRespDTO.ReadyResponseDTO.class) + .block(java.time.Duration.ofSeconds(10)); + + if (response == null) { + log.error("[KakaoPayClient] ready 응답이 null"); + throw new GeneralException(ErrorStatus.KAKAOPAY_READY_FAILED); + } + + log.info("[KakaoPayClient] ready 호출 성공 - tid={}", response.getTid()); + return response; + + } catch (org.springframework.web.reactive.function.client.WebClientResponseException e) { + log.error("[KakaoPayClient] ready 실패 - status={}, body={}", + e.getStatusCode(), e.getResponseBodyAsString(), e); + throw new GeneralException(ErrorStatus.KAKAOPAY_READY_FAILED); + } catch (Exception e) { + log.error("[KakaoPayClient] ready 호출 실패", e); + throw new GeneralException(ErrorStatus.KAKAOPAY_READY_FAILED); + } + } + + public KakaoPayRespDTO.ApproveResponseDTO approve(KakaoPayReqDTO.ApproveRequestDTO request) { + try { + return kakaoPayWebClient.post() + .uri(kakaoPayProperties.getBaseUrl() + "/online/v1/payment/approve") + .header(HttpHeaders.AUTHORIZATION, "SECRET_KEY " + kakaoPayProperties.getSecretKey()) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(request) + .retrieve() + .bodyToMono(KakaoPayRespDTO.ApproveResponseDTO.class) + .block(); + } catch (Exception e) { + throw new GeneralException(ErrorStatus.KAKAOPAY_APPROVE_FAILED); + } + } +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/service/ReservationService.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/service/ReservationService.java index ce1635e..dc0041d 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/service/ReservationService.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/service/ReservationService.java @@ -1,5 +1,6 @@ package Comprehensive_Design_Project.CUK_Compasser.domain.reservation.service; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.dto.KakaoPayRespDTO; import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.dto.ReservationReqDTO; import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.dto.ReservationRespDTO; @@ -11,6 +12,11 @@ public interface ReservationService { ReservationRespDTO.ReservationDTO approveReservation(Long reservationId, Long memberId); - ReservationRespDTO.ReservationDTO rejectReservation(Long reservationId, Long memberId, - ReservationReqDTO request); + ReservationRespDTO.ReservationDTO rejectReservation(Long reservationId, Long memberId, ReservationReqDTO request); + + KakaoPayRespDTO.ReadyResultDTO readyKakaoPay(Long reservationId, Long memberId); + + KakaoPayRespDTO.ApproveResultDTO approveKakaoPay(Long reservationId, Long memberId, String pgToken); + + void cancelKakaoPay(Long reservationId, Long memberId); } \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/service/ReservationServiceImpl.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/service/ReservationServiceImpl.java index a679853..3836e5f 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/service/ReservationServiceImpl.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/reservation/service/ReservationServiceImpl.java @@ -1,18 +1,25 @@ package Comprehensive_Design_Project.CUK_Compasser.domain.reservation.service; +import Comprehensive_Design_Project.CUK_Compasser.domain.randomBox.entity.RandomBox; +import Comprehensive_Design_Project.CUK_Compasser.domain.randomBox.repository.RandomBoxRepository; import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.converter.ReservationConverter; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.dto.KakaoPayReqDTO; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.dto.KakaoPayRespDTO; import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.dto.ReservationReqDTO; import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.dto.ReservationRespDTO; -import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity.Reservation; -import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity.ReservationStatus; +import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.entity.*; import Comprehensive_Design_Project.CUK_Compasser.domain.reservation.repository.ReservationRepository; import Comprehensive_Design_Project.CUK_Compasser.domain.store.entity.Store; import Comprehensive_Design_Project.CUK_Compasser.domain.store.repository.StoreRepository; import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.code.status.ErrorStatus; import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.exception.GeneralException; +import Comprehensive_Design_Project.CUK_Compasser.global.config.KakaoPayProperties; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.StringUtils; import java.util.List; @@ -24,22 +31,15 @@ public class ReservationServiceImpl implements ReservationService { private final ReservationRepository reservationRepository; private final StoreRepository storeRepository; + private final RandomBoxRepository randomBoxRepository; private final ReservationConverter reservationConverter; + private final KakaoPayClient kakaoPayClient; + private final KakaoPayProperties kakaoPayProperties; + private final PlatformTransactionManager transactionManager; - /** - * 예약 페이지 목록 조회 - * - * - 예약 페이지에는 확인 대기중인 예약만 보여준다. - * - 즉 REQUESTED 상태의 예약만 조회한다. - * - * 주의: - * - 이 메서드는 현재 로그인한 점장의 가게 예약만 조회된다는 전제를 가진다. - * - memberId를 직접 사용하지 않는 구조라면, 로그인단/상위 계층에서 이미 필터링된 상태여야 한다. - */ @Override @Transactional(readOnly = true) public ReservationRespDTO.ReservationListDTO getPendingReservations(Long memberId) { - Store store = storeRepository.findByStoreManager_MemberId(memberId) .orElseThrow(() -> new GeneralException(ErrorStatus.STORE_NOT_FOUND)); @@ -51,16 +51,6 @@ public ReservationRespDTO.ReservationListDTO getPendingReservations(Long memberI return reservationConverter.toReservationListDTO(reservations); } - /** - * 주문 페이지 목록 조회 - * - * - 주문 페이지에는 처리 완료된 예약만 보여준다. - * - 즉 APPROVED, REJECTED, CANCELED 상태를 조회한다. - * - * 주의: - * - 현재 구조상 memberId가 실제로 storeId 역할을 하도록 쓰면 안 된다. - * - 이 메서드를 그대로 쓰려면 상위에서 실제 storeId를 넘겨주도록 다시 정리하는 것이 가장 안전하다. - */ @Override @Transactional(readOnly = true) public ReservationRespDTO.ReservationListDTO getProcessedReservations(Long memberId) { @@ -81,12 +71,6 @@ public ReservationRespDTO.ReservationListDTO getProcessedReservations(Long membe return reservationConverter.toReservationListDTO(reservations); } - /** - * 예약 수락 - * - * - REQUESTED 상태의 예약만 APPROVED로 변경할 수 있다. - * - 수락 시 rejectReason은 null로 초기화한다. - */ @Override @Transactional public ReservationRespDTO.ReservationDTO approveReservation(Long reservationId, Long memberId) { @@ -96,45 +80,215 @@ public ReservationRespDTO.ReservationDTO approveReservation(Long reservationId, Reservation reservation = reservationRepository.findByIdAndStore_Id(reservationId, store.getId()) .orElseThrow(() -> new GeneralException(ErrorStatus.RESERVATION_NOT_FOUND)); - if (reservation.getStatus() == ReservationStatus.CANCELED) { + ReservationStatus currentStatus = reservation.getStatus(); + + if (reservation.getPickupStatus() == PickupStatus.PICKED_UP) { + throw new GeneralException(ErrorStatus.RESERVATION_ALREADY_PICKED_UP); + } + + if (currentStatus == ReservationStatus.CANCELED) { throw new GeneralException(ErrorStatus.INVALID_RESERVATION_STATUS); } - reservation.setStatus(ReservationStatus.APPROVED); - reservation.setRejectReason(null); + if (currentStatus == ReservationStatus.APPROVED) { + throw new GeneralException(ErrorStatus.RESERVATION_ALREADY_APPROVED); + } + + RandomBox lockedRandomBox = randomBoxRepository.findWithLockById(reservation.getRandomBox().getId()) + .orElseThrow(() -> new GeneralException(ErrorStatus.RANDOM_BOX_NOT_FOUND)); + + lockedRandomBox.decreaseStock(reservation.getRequestedQuantity()); + + reservation.approve(); + reservation.markPreparing(); return reservationConverter.toReservationDTO(reservation); } - /** - * 예약 거절 - * - * - request.status 값이 반드시 있어야 한다. - * - 현재 예약은 REQUESTED 상태일 때만 처리할 수 있다. - * - APPROVED면 수락 처리 - * - REJECTED면 거절 처리하며 rejectReason은 필수다. - * - 그 외 상태값은 점장 처리용 상태가 아니므로 예외 처리한다. - */ + @Override @Transactional - public ReservationRespDTO.ReservationDTO rejectReservation(Long reservationId, Long memberId, - ReservationReqDTO request) { + public ReservationRespDTO.ReservationDTO rejectReservation(Long reservationId, Long memberId, ReservationReqDTO request) { Store store = storeRepository.findByStoreManager_MemberId(memberId) .orElseThrow(() -> new GeneralException(ErrorStatus.STORE_NOT_FOUND)); Reservation reservation = reservationRepository.findByIdAndStore_Id(reservationId, store.getId()) .orElseThrow(() -> new GeneralException(ErrorStatus.RESERVATION_NOT_FOUND)); - if (reservation.getStatus() == ReservationStatus.CANCELED) { + ReservationStatus currentStatus = reservation.getStatus(); + + if (request.getRejectReason() == null || request.getRejectReason().trim().isEmpty()) { + throw new GeneralException(ErrorStatus.REJECT_REASON_REQUIRED); + } + + if (reservation.getPickupStatus() == PickupStatus.PICKED_UP) { + throw new GeneralException(ErrorStatus.RESERVATION_ALREADY_PICKED_UP); + } + + if (currentStatus == ReservationStatus.CANCELED) { throw new GeneralException(ErrorStatus.INVALID_RESERVATION_STATUS); } - if (!StringUtils.hasText(request.getRejectReason())) { - throw new GeneralException(ErrorStatus.REJECT_REASON_REQUIRED); + if (currentStatus == ReservationStatus.REJECTED) { + throw new GeneralException(ErrorStatus.RESERVATION_ALREADY_REJECTED); } - reservation.setStatus(ReservationStatus.REJECTED); - reservation.setRejectReason(request.getRejectReason().trim()); + if (currentStatus == ReservationStatus.APPROVED) { + RandomBox lockedRandomBox = randomBoxRepository.findWithLockById(reservation.getRandomBox().getId()) + .orElseThrow(() -> new GeneralException(ErrorStatus.RANDOM_BOX_NOT_FOUND)); + + lockedRandomBox.increaseStock(reservation.getRequestedQuantity()); + } + + reservation.reject(request.getRejectReason()); return reservationConverter.toReservationDTO(reservation); } + + @Override + @Transactional + public KakaoPayRespDTO.ReadyResultDTO readyKakaoPay(Long reservationId, Long memberId) { + Reservation reservation = reservationRepository.findByIdAndMember_Id(reservationId, memberId) + .orElseThrow(() -> new GeneralException(ErrorStatus.RESERVATION_NOT_FOUND)); + + if (reservation.getPaymentStatus() == PaymentStatus.PAID) { + throw new GeneralException(ErrorStatus.RESERVATION_ALREADY_PAID); + } + + if (reservation.getStatus() == ReservationStatus.CANCELED || + reservation.getStatus() == ReservationStatus.REJECTED) { + throw new GeneralException(ErrorStatus.INVALID_RESERVATION_STATUS); + } + + String partnerOrderId = "reservation_" + reservation.getId(); + String partnerUserId = "member_" + memberId; + + KakaoPayReqDTO.ReadyRequestDTO request = KakaoPayReqDTO.ReadyRequestDTO.builder() + .cid(kakaoPayProperties.getCid()) + .partner_order_id(partnerOrderId) + .partner_user_id(partnerUserId) + .item_name(reservation.getStore().getStoreName() + " 랜덤박스") + .quantity(reservation.getRequestedQuantity()) + .total_amount(reservation.getTotalPrice()) + .tax_free_amount(0) + .approval_url(kakaoPayProperties.getApprovalUrl() + "?reservationId=" + reservation.getId()) + .cancel_url(kakaoPayProperties.getCancelUrl() + "?reservationId=" + reservation.getId()) + .fail_url(kakaoPayProperties.getFailUrl() + "?reservationId=" + reservation.getId()) + .build(); + + KakaoPayRespDTO.ReadyResponseDTO response = kakaoPayClient.ready(request); + + reservation.markPaymentReady(response.getTid(), "KAKAOPAY"); + + return KakaoPayRespDTO.ReadyResultDTO.builder() + .reservationId(reservation.getId()) + .tid(response.getTid()) + .redirectUrl(response.getNext_redirect_pc_url()) + .paymentStatus(reservation.getPaymentStatus().name()) + .build(); + } + + @Override + @Transactional + public KakaoPayRespDTO.ApproveResultDTO approveKakaoPay(Long reservationId, Long memberId, String pgToken) { + if (!StringUtils.hasText(pgToken)) { + throw new GeneralException(ErrorStatus.INVALID_PG_TOKEN); + } + + Reservation reservation = reservationRepository.findByIdAndMember_Id(reservationId, memberId) + .orElseThrow(() -> new GeneralException(ErrorStatus.RESERVATION_NOT_FOUND)); + + if (reservation.getStatus() == ReservationStatus.CANCELED || + reservation.getStatus() == ReservationStatus.REJECTED) { + throw new GeneralException(ErrorStatus.INVALID_RESERVATION_STATUS); + } + + if (!StringUtils.hasText(reservation.getPaymentTid())) { + throw new GeneralException(ErrorStatus.PAYMENT_TID_NOT_FOUND); + } + + if (reservation.getPaymentStatus() == PaymentStatus.PAID) { + throw new GeneralException(ErrorStatus.RESERVATION_ALREADY_PAID); + } + + if (reservation.getPaymentStatus() != PaymentStatus.READY) { + throw new GeneralException(ErrorStatus.INVALID_PAYMENT_STATUS); + } + + try { + KakaoPayReqDTO.ApproveRequestDTO request = KakaoPayReqDTO.ApproveRequestDTO.builder() + .cid(kakaoPayProperties.getCid()) + .tid(reservation.getPaymentTid()) + .partner_order_id("reservation_" + reservation.getId()) + .partner_user_id("member_" + memberId) + .pg_token(pgToken) + .build(); + + KakaoPayRespDTO.ApproveResponseDTO response = kakaoPayClient.approve(request); + + validateApproveResponse(reservation, response); + + reservation.markPaid("KAKAOPAY"); + + return KakaoPayRespDTO.ApproveResultDTO.builder() + .reservationId(reservation.getId()) + .paymentMethod(reservation.getPaymentMethod()) + .paymentStatus(reservation.getPaymentStatus().name()) + .approvedAt(response.getApproved_at()) + .build(); + + } catch (GeneralException e) { + markPaymentFailedInNewTx(reservation.getId()); + throw e; + } catch (Exception e) { + markPaymentFailedInNewTx(reservation.getId()); + throw new GeneralException(ErrorStatus.KAKAOPAY_APPROVE_FAILED); + } + } + + private void markPaymentFailedInNewTx(Long reservationId) { + TransactionTemplate txTemplate = new TransactionTemplate(transactionManager); + txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + txTemplate.executeWithoutResult(status -> { + Reservation failedReservation = reservationRepository.findById(reservationId) + .orElseThrow(() -> new GeneralException(ErrorStatus.RESERVATION_NOT_FOUND)); + + failedReservation.markPaymentFailed(); + }); + } + + private void validateApproveResponse(Reservation reservation, KakaoPayRespDTO.ApproveResponseDTO response) { + if (response == null || response.getAmount() == null || response.getAmount().getTotal() == null) { + throw new GeneralException(ErrorStatus.INVALID_PAYMENT_AMOUNT); + } + + Integer approvedAmount = response.getAmount().getTotal(); + Integer expectedAmount = reservation.getTotalPrice(); + + if (!expectedAmount.equals(approvedAmount)) { + throw new GeneralException(ErrorStatus.INVALID_PAYMENT_AMOUNT); + } + + if (!reservation.getPaymentTid().equals(response.getTid())) { + throw new GeneralException(ErrorStatus.INVALID_PAYMENT_INFO); + } + + String expectedPartnerOrderId = "reservation_" + reservation.getId(); + if (!expectedPartnerOrderId.equals(response.getPartner_order_id())) { + throw new GeneralException(ErrorStatus.INVALID_PAYMENT_INFO); + } + } + + @Override + @Transactional + public void cancelKakaoPay(Long reservationId, Long memberId) { + Reservation reservation = reservationRepository.findByIdAndMember_Id(reservationId, memberId) + .orElseThrow(() -> new GeneralException(ErrorStatus.RESERVATION_NOT_FOUND)); + + if (reservation.getPaymentStatus() == PaymentStatus.PAID) { + throw new GeneralException(ErrorStatus.RESERVATION_ALREADY_PAID); + } + + reservation.cancelPayment(); + } } \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/controller/StoreController.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/controller/StoreController.java index 06ae0b9..5d6fac7 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/controller/StoreController.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/controller/StoreController.java @@ -74,8 +74,17 @@ public ApiResponse> getStoreList ( return ApiResponse.onSuccess(SuccessStatus.OK, storeService.getStoreList(userLat, userLon, page)); } + @GetMapping("/address") + @Operation(summary = "특정 주소 기준 가게 조회 API", description = "사용자가 입력한 특정 주소를 기준으로 하는 가게 조회 API로, createdAt 기준 페이지네이션으로 10개 씩 반환하는 API 입니다.") + public ApiResponse getStoreListByAddress( + /*@AuthenticationPrincipal CustomUserDetails userDetails,*/ + @RequestParam String address, + @RequestParam(defaultValue = "0") Integer page){ + return ApiResponse.onSuccess(SuccessStatus.OK, storeService.getStoreListByAddress(address, page)); + } + @GetMapping("/tag/{tag}") - @Operation(summary = "태그 별 가게 조회 API", description = "사용자가 고른 태그를 기준으로 가게를 페이지네이션 조회를 하는 API 입니다.") + @Operation(summary = "태그 별 가게 조회 API", description = "사용자가 고른 태그를 기준으로 가게를 페이지네이션 조회를 하는 API 입니다. ") public ApiResponse> getStoreListByTag ( /*@AuthenticationPrincipal CustomUserDetails userDetails,*/ @RequestParam BigDecimal userLat, @@ -85,8 +94,10 @@ public ApiResponse> getStoreListByTag ( return ApiResponse.onSuccess(SuccessStatus.OK, storeService.getStoreListByTag(userLat, userLon, tag, page)); } - @GetMapping("/university/{university}") - @Operation(summary = "대학교 반경 가게 조회 API", description = "사용자가 고른 대학교를 기준으로 반경의 가게를 조회하는 API 입니다.") + + /* 대학교도 특정 주소이므로, 해당 API는 안 쓸 듯? */ +// @GetMapping("/university/{university}") +// @Operation(summary = "대학교 반경 가게 조회 API", description = "사용자가 고른 대학교를 기준으로 반경의 가게를 조회하는 API 입니다.") public ApiResponse> getStoreListByUniversity ( /*@AuthenticationPrincipal CustomUserDetails userDetails,*/ @RequestBody StoreReqDTO.StoreReqWithCoordinateAndTagDTO dto, @@ -103,19 +114,19 @@ public ApiResponse> getStoreListByMemberRadius ( } @GetMapping("/{storeId}/simple") - @Operation(summary = "가게 단순 조회 API", description = "사용자 원하는 가게 대한 단순 정보를 요청/반환하는 API 입니다.") + @Operation(summary = "가게 단순 조회 API", description = "사용자 원하는 가게 대한 단순 정보를 요청/반환하는 API 입니다. 화면 기준 핀 클릭해서 나오게 되는 바텀 시트 바용 API 입니다!") public ApiResponse getStoreSimple (@PathVariable Long storeId){ return ApiResponse.onSuccess(SuccessStatus.OK, storeService.getSimpleStoreInfo(storeId)); } @GetMapping("/{storeId}") - @Operation(summary = "가게 상세 조회 API", description = "사용자 원하는 가게 대한 상세 정보를 요청/반환하는 API 입니다.") + @Operation(summary = "가게 상세 조회 API", description = "사용자 원하는 가게 대한 상세 정보를 요청/반환하는 API 입니다. 바텀 시트 바에서 상점 보러가기를 통해 상세 정보를 받는 API 입니다!") public ApiResponse getStore (@PathVariable Long storeId){ return ApiResponse.onSuccess(SuccessStatus.OK, storeService.getStoreInfo(storeId)); } - @GetMapping("/keyword") - @Operation(summary = "가게 검색 API", description = "사용자 원하는 가게 대한 상세 정보를 요청/반환하는 API 입니다.") +// @GetMapping("/keyword") +// @Operation(summary = "가게 검색 API", description = "사용자가 입력한 키워드를 사용자 원하는 가게 대한 상세 정보를 요청/반환하는 API 입니다. 이건 제가 착각해서 가게 이름을 사용자가 '직접' 입력해서 검색하는 API로 만들어놔서 안쓸 것 같아요...") public ApiResponse> getStoreListByKeyword ( @RequestParam BigDecimal userLat, @RequestParam BigDecimal userLon, diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/converter/StoreConverter.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/converter/StoreConverter.java index ad4fdca..b790885 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/converter/StoreConverter.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/converter/StoreConverter.java @@ -59,14 +59,19 @@ public List toGetStoreDTOList(List sto List dtoList = new ArrayList<>(); for (Store store : storeList) { + String imageUrl = (store.getImages() != null && !store.getImages().isEmpty()) + ? store.getImages().get(0).getImageUrl() + : null; + dtoList.add(StoreRespPagingDTO.GetStoreReqDTO.builder() - .storeId(store.getId()) + .storeId(store.getId()) // 맵 화면에서 사용할 id -> 심플 조회 (바텀시트바) API 에 활용 .storeManagerId(store.getStoreManager().getId()) .storeName(store.getStoreName()) + .storeImage(imageUrl) .tag(store.getTag()) .storeDetails(store.getStoreDetails()) - .latitude(store.getLatitude()) - .longitude(store.getLongitude()) + .latitude(store.getLatitude()) // 맵 화면에서 사용할 위도 -> 심플 조회 (바텀시트바) API 에 활용 + .longitude(store.getLongitude()) // 맵 화면에서 사용할 경도 -> 심플 조회 (바텀시트바) API 에 활용 .businessHours(toJsonNode(store.getBusinessHours())) .build()); } diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/dto/resp/StoreRespPagingDTO.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/dto/resp/StoreRespPagingDTO.java index 1ad3e5f..9d84f76 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/dto/resp/StoreRespPagingDTO.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/dto/resp/StoreRespPagingDTO.java @@ -16,6 +16,7 @@ public static class GetStoreReqDTO { private Long storeId; private Long storeManagerId; private String storeName; + private String storeImage; private Tag tag; private String storeDetails; private BigDecimal latitude; diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/repository/StoreRepository.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/repository/StoreRepository.java index c588660..985045e 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/repository/StoreRepository.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/repository/StoreRepository.java @@ -56,7 +56,7 @@ Page findStoresWithinRadius( Page findStoresByTagWithinRadius( @Param("userLon") BigDecimal userLon, @Param("userLat") BigDecimal userLat, - @Param("distance") double distanceInMeters, + @Param("distance") double distance, @Param("tag") String tag, Pageable pageable); // 태그 별 조회 + 반경 + 페이징 @@ -71,7 +71,7 @@ Page findStoresByTagWithinRadius( Page findStoresByKeywordWithinRadius( @Param("userLon") BigDecimal userLon, @Param("userLat") BigDecimal userLat, - @Param("distance") double distanceInMeters, + @Param("distance") double distance, @Param("keyword") String keyword, Pageable pageable); // 가게 이름 조회 + 반경 + 페이징 diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/service/StoreService.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/service/StoreService.java index c5c9ff0..46d6805 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/service/StoreService.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/service/StoreService.java @@ -115,7 +115,21 @@ public List getStoreListByTag (BigDecimal use return storeConverter.toGetStoreDTOList(storeList); } - @Transactional (readOnly = true) + @Transactional(readOnly = true) + public List getStoreListByAddress (String address, Integer page) { + + KakaoAddressSearchRespDTO.Document document = kakaoLocalService.searchAddress(address); + + List storeList = storeRepository.findStoresWithinRadius(BigDecimal.valueOf( + Double.parseDouble(document.getX())), + BigDecimal.valueOf(Double.parseDouble(document.getY())), + 3000, + PageRequest.of(page, 10)).getContent(); + + return storeConverter.toGetStoreDTOList(storeList); + } + + @Transactional (readOnly = true) // -> 대학교도 '특정 주소'이므로 굳이? public List getStoreListByUniversity (String email, String university){ return null; diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/storeManager/entity/StoreManager.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/storeManager/entity/StoreManager.java index 6781171..a2e5ea8 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/storeManager/entity/StoreManager.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/storeManager/entity/StoreManager.java @@ -1,5 +1,6 @@ package Comprehensive_Design_Project.CUK_Compasser.domain.storeManager.entity; +import Comprehensive_Design_Project.CUK_Compasser.domain.member.entity.BankType; import Comprehensive_Design_Project.CUK_Compasser.domain.member.entity.Member; import Comprehensive_Design_Project.CUK_Compasser.domain.store.entity.Store; import Comprehensive_Design_Project.CUK_Compasser.global.common.BaseEntity; @@ -37,6 +38,16 @@ public class StoreManager extends BaseEntity { @Column(name = "verified_at") private LocalDateTime verifiedAt; + @Enumerated(EnumType.STRING) + @Column(name = "deposit_bank_type", length = 30) + private BankType depositBankType; + + @Column(name = "deposit_account_number", length = 50) + private String depositAccountNumber; + + @Column(name = "deposit_account_holder", length = 50) + private String depositAccountHolder; + @OneToOne(mappedBy = "storeManager", fetch = FetchType.LAZY) private Store store; } \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/common/apiPayload/code/status/ErrorStatus.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/common/apiPayload/code/status/ErrorStatus.java index dc2d85a..ea839de 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/common/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/common/apiPayload/code/status/ErrorStatus.java @@ -64,6 +64,9 @@ public enum ErrorStatus implements BaseErrorCode { INVALID_RANDOM_BOX_BUY_LIMIT(HttpStatus.BAD_REQUEST, "RB004", "구매 제한 개수가 올바르지 않습니다."), INVALID_RANDOM_BOX_SALE_STATUS(HttpStatus.BAD_REQUEST, "RB005", "랜덤박스 판매 상태가 올바르지 않습니다."), RANDOM_BOX_NOT_FOUND(HttpStatus.NOT_FOUND, "RB006", "랜덤박스를 찾을 수 없습니다."), + RANDOM_BOX_NOT_ON_SALE(HttpStatus.BAD_REQUEST, "RB007", "판매 중인 랜덤박스가 아닙니다."), + RANDOM_BOX_STOCK_NOT_ENOUGH(HttpStatus.BAD_REQUEST, "RB008", "랜덤박스 재고가 부족합니다."), + RANDOM_BOX_BUY_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "RB009", "구매 가능 수량을 초과했습니다."), // ========================= // [QRCode] @@ -94,9 +97,31 @@ public enum ErrorStatus implements BaseErrorCode { RESERVATION_ALREADY_PROCESSED(HttpStatus.BAD_REQUEST, "R002", "이미 처리된 예약입니다."), REJECT_REASON_REQUIRED(HttpStatus.BAD_REQUEST, "R003", "거절 사유는 필수입니다."), RESERVATION_STATUS_REQUIRED(HttpStatus.BAD_REQUEST, "R004", "예약 상태값은 필수입니다."), - INVALID_RESERVATION_STATUS(HttpStatus.BAD_REQUEST, "R005", "변경할 수 없는 예약 상태입니다."); + INVALID_RESERVATION_STATUS(HttpStatus.BAD_REQUEST, "R005", "변경할 수 없는 예약 상태입니다."), + RESERVATION_ALREADY_APPROVED(HttpStatus.BAD_REQUEST, "R006", "이미 승인된 예약입니다."), + RESERVATION_ALREADY_REJECTED(HttpStatus.BAD_REQUEST, "R007", "이미 거절된 예약입니다."), + RESERVATION_ALREADY_PICKED_UP(HttpStatus.BAD_REQUEST, "R008", "이미 픽업된 예약입니다."), + RESERVATION_ALREADY_PAID(HttpStatus.CONFLICT, "R010", "이미 결제가 완료된 예약입니다."), + // ========================= + // [Order] + // ========================= + INVALID_ORDER_QUANTITY(HttpStatus.BAD_REQUEST, "O001", "주문 수량이 올바르지 않습니다."), + ORDER_ALREADY_PAID(HttpStatus.BAD_REQUEST, "O002", "이미 송금 완료 처리된 주문입니다."), + ORDER_CANCEL_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "O003", "현재 상태의 주문은 취소할 수 없습니다."), + + // ========================= + // [Payment] + // ========================= + KAKAOPAY_READY_FAILED(HttpStatus.BAD_GATEWAY, "P001", "카카오페이 결제 준비에 실패했습니다."), + KAKAOPAY_APPROVE_FAILED(HttpStatus.BAD_GATEWAY, "P002", "카카오페이 결제 승인에 실패했습니다."), + PAYMENT_TID_NOT_FOUND(HttpStatus.NOT_FOUND, "P003", "결제 TID를 찾을 수 없습니다."), + INVALID_PG_TOKEN(HttpStatus.BAD_REQUEST, "P004", "유효하지 않은 pg_token 입니다."), + INVALID_PAYMENT_STATUS(HttpStatus.BAD_REQUEST, "P005", "현재 결제 상태에서는 요청할 수 없습니다."), + INVALID_PAYMENT_AMOUNT(HttpStatus.BAD_REQUEST, "P006", "결제 금액이 주문 금액과 일치하지 않습니다."), + INVALID_PAYMENT_INFO(HttpStatus.BAD_REQUEST, "P007", "결제 승인 정보가 올바르지 않습니다."); + private final HttpStatus httpStatus; private final String code; private final String message; diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/common/apiPayload/code/status/SuccessStatus.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/common/apiPayload/code/status/SuccessStatus.java index 6a37bae..bf8811b 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/common/apiPayload/code/status/SuccessStatus.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/common/apiPayload/code/status/SuccessStatus.java @@ -9,7 +9,21 @@ @RequiredArgsConstructor public enum SuccessStatus implements BaseSuccessCode { OK(HttpStatus.OK, "S000", "요청이 성공했습니다."), - CREATED(HttpStatus.CREATED, "S001", "생성되었습니다."); + CREATED(HttpStatus.CREATED, "S001", "생성되었습니다."), + + // ========================= + // [Order] + // ========================= + ORDER_CREATED(HttpStatus.CREATED, "O2001", "주문 생성에 성공했습니다."), + ORDER_COMPLETED(HttpStatus.OK, "O2002", "결제 진행 정보 저장에 성공했습니다."), + ORDER_STATUS_FOUND(HttpStatus.OK, "O2003", "주문 상태 조회에 성공했습니다."), + ORDER_CANCELED(HttpStatus.OK, "ORDER2004", "주문 취소에 성공했습니다."), + + // ========================= + // [Payment] + // ========================= + RESERVATION_PAYMENT_READY(HttpStatus.OK, "R2004", "카카오페이 결제 준비에 성공했습니다."), + RESERVATION_PAYMENT_APPROVED(HttpStatus.OK, "R2005", "카카오페이 결제 승인에 성공했습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/config/KakaoWebClientConfig.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/config/KakaoLocalWebClientConfig.java similarity index 95% rename from src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/config/KakaoWebClientConfig.java rename to src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/config/KakaoLocalWebClientConfig.java index 7616ace..3bb6aab 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/config/KakaoWebClientConfig.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/config/KakaoLocalWebClientConfig.java @@ -8,7 +8,7 @@ @Configuration @RequiredArgsConstructor -public class KakaoWebClientConfig { +public class KakaoLocalWebClientConfig { @Value("${kakao.local.base-url}") private String baseUrl; diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/config/KakaoPayProperties.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/config/KakaoPayProperties.java new file mode 100644 index 0000000..51d4047 --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/config/KakaoPayProperties.java @@ -0,0 +1,20 @@ +package Comprehensive_Design_Project.CUK_Compasser.global.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "kakaopay") +public class KakaoPayProperties { + + private String cid; + private String secretKey; + private String baseUrl; + private String approvalUrl; + private String cancelUrl; + private String failUrl; +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/config/KakaoPayWebClientConfig.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/config/KakaoPayWebClientConfig.java new file mode 100644 index 0000000..6a09c5d --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/config/KakaoPayWebClientConfig.java @@ -0,0 +1,14 @@ +package Comprehensive_Design_Project.CUK_Compasser.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class KakaoPayWebClientConfig { + + @Bean + public WebClient kakaoPayWebClient(WebClient.Builder builder) { + return builder.build(); + } +} \ No newline at end of file diff --git a/src/main/resources/TestData.sql b/src/main/resources/TestData.sql index 2fd488b..c789982 100644 --- a/src/main/resources/TestData.sql +++ b/src/main/resources/TestData.sql @@ -1,39 +1,369 @@ -insert into members -(created_at, updated_at, email, member_name, password, phone, provider, provider_id, role, status) VALUES - (NOW(), now(), 'test@email.com', 'testname', 'password', '01012345678', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'); +-- --------------------------------------------------------- +-- 1. members 데이터 삽입 (ID 1~40) +-- --------------------------------------------------------- +INSERT INTO members (created_at, updated_at, email, member_name, password, phone, provider, provider_id, role, status) +VALUES (NOW(), NOW(), 'manager1@test.com', '성수매니저', 'password', '010-1111-1111', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'manager2@test.com', '강남매니저', 'password', '010-2222-2222', 'NORMAL', NULL, 'STORE_MANAGER','ACTIVE'), + (NOW(), NOW(), 'manager3@test.com', '연남매니저', 'password', '010-3333-3333', 'NORMAL', NULL, 'STORE_MANAGER','ACTIVE'), + (NOW(), NOW(), 'manager4@test.com', '이태원매니저', 'password', '010-4444-4444', 'NORMAL', NULL, 'STORE_MANAGER','ACTIVE'), + (NOW(), NOW(), 'manager5@test.com', '판교매니저', 'password', '010-5555-5555', 'NORMAL', NULL, 'STORE_MANAGER','ACTIVE'), + (NOW(), NOW(), 'm6@test.com', '건대매니저', 'pass', '010-0006', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm7@test.com', '왕십리매니저', 'pass', '010-0007', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm8@test.com', '잠실매니저', 'pass', '010-0008', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm9@test.com', '서울숲매니저', 'pass', '010-0009', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm10@test.com', '한양대매니저', 'pass', '010-0010', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm11@test.com', '압구정매니저', 'pass', '010-0011', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm12@test.com', '논현매니저', 'pass', '010-0012', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm13@test.com', '청담매니저', 'pass', '010-0013', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm14@test.com', '천호매니저', 'pass', '010-0014', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm15@test.com', '군자매니저', 'pass', '010-0015', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm16@test.com', '뚝섬매니저', 'pass', '010-0016', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm17@test.com', '자양매니저', 'pass', '010-0017', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm18@test.com', '구의매니저', 'pass', '010-0018', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm19@test.com', '화양매니저', 'pass', '010-0019', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm20@test.com', '마장매니저', 'pass', '010-0020', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm21@test.com', '장안매니저', 'pass', '010-0021', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm22@test.com', '삼성매니저', 'pass', '010-0022', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm23@test.com', '회기매니저', 'pass', '010-0023', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm24@test.com', '가락매니저', 'pass', '010-0024', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm25@test.com', '대치매니저', 'pass', '010-0025', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm26@test.com', '역곡매니저', 'pass', '010-0026', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm27@test.com', '소사매니저', 'pass', '010-0027', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm28@test.com', '온수매니저', 'pass', '010-0028', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm29@test.com', '부천종합매니저', 'pass', '010-0029', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm30@test.com', '궁동매니저', 'pass', '010-0030', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm31@test.com', '범박매니저', 'pass', '010-0031', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm32@test.com', '개봉매니저', 'pass', '010-0032', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm33@test.com', '천왕매니저', 'pass', '010-0033', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm34@test.com', '중동매니저', 'pass', '010-0034', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm35@test.com', '광명사거리매니저', 'pass', '010-0035', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm36@test.com', '오류매니저', 'pass', '010-0036', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm37@test.com', '고척매니저', 'pass', '010-0037', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm38@test.com', '철산매니저', 'pass', '010-0038', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm39@test.com', '옥길매니저', 'pass', '010-0039', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), + (NOW(), NOW(), 'm40@test.com', '부천시청매니저', 'pass', '010-0040', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'); -insert into store_managers (created_at, member_id, updated_at, verified_at, business_license_number) -values (now(), 1, now(), now(), 'test-number'); +-- --------------------------------------------------------- +-- 2. store_managers 데이터 삽입 (member_id와 1:1 매칭) +-- --------------------------------------------------------- +INSERT INTO store_managers (created_at, member_id, updated_at, verified_at, business_license_number) +VALUES (NOW(), 1, NOW(), NOW(), '111-81-12345'), + (NOW(), 2, NOW(), NOW(), '222-82-23456'), + (NOW(), 3, NOW(), NOW(), '333-83-34567'), + (NOW(), 4, NOW(), NOW(), '444-84-45678'), + (NOW(), 5, NOW(), NOW(), '555-85-56789'), + (NOW(), 6, NOW(), NOW(), '666-06'), + (NOW(), 7, NOW(), NOW(), '777-07'), + (NOW(), 8, NOW(), NOW(), '888-08'), + (NOW(), 9, NOW(), NOW(), '999-09'), + (NOW(), 10, NOW(), NOW(), '000-10'), + (NOW(), 11, NOW(), NOW(), '111-11'), + (NOW(), 12, NOW(), NOW(), '222-12'), + (NOW(), 13, NOW(), NOW(), '333-13'), + (NOW(), 14, NOW(), NOW(), '444-14'), + (NOW(), 15, NOW(), NOW(), '555-15'), + (NOW(), 16, NOW(), NOW(), '160-16'), + (NOW(), 17, NOW(), NOW(), '170-17'), + (NOW(), 18, NOW(), NOW(), '180-18'), + (NOW(), 19, NOW(), NOW(), '190-19'), + (NOW(), 20, NOW(), NOW(), '200-20'), + (NOW(), 21, NOW(), NOW(), '210-21'), + (NOW(), 22, NOW(), NOW(), '220-22'), + (NOW(), 23, NOW(), NOW(), '230-23'), + (NOW(), 24, NOW(), NOW(), '240-24'), + (NOW(), 25, NOW(), NOW(), '250-25'), + (NOW(), 26, NOW(), NOW(), '260-26'), + (NOW(), 27, NOW(), NOW(), '270-27'), + (NOW(), 28, NOW(), NOW(), '280-28'), + (NOW(), 29, NOW(), NOW(), '290-29'), + (NOW(), 30, NOW(), NOW(), '300-30'), + (NOW(), 31, NOW(), NOW(), '310-31'), + (NOW(), 32, NOW(), NOW(), '320-32'), + (NOW(), 33, NOW(), NOW(), '330-33'), + (NOW(), 34, NOW(), NOW(), '340-34'), + (NOW(), 35, NOW(), NOW(), '350-35'), + (NOW(), 36, NOW(), NOW(), '360-36'), + (NOW(), 37, NOW(), NOW(), '370-37'), + (NOW(), 38, NOW(), NOW(), '380-38'), + (NOW(), 39, NOW(), NOW(), '390-39'), + (NOW(), 40, NOW(), NOW(), '400-40'); -INSERT INTO stores ( - latitude, longitude, created_at, updated_at, +-- --------------------------------------------------------- +-- 3. stores 데이터 삽입 (store_manager_id와 1:1 매칭) +-- 일반 데이터 기준: 37.5445, 127.0560 +-- 가톨릭대 정문: 37.4855, 126.8025 +-- --------------------------------------------------------- +INSERT INTO stores (latitude, longitude, created_at, updated_at, store_manager_id, store_name, store_details, business_hours, input_address, jibun_address, road_address, store_email, tag -) -VALUES --- 1번 매장: 성수동 힙한 베이커리 -( - 37.5445770, 127.0559740, NOW(), NOW(), +) VALUES +-- 그룹 1: 성수 (매니저 1) +(37.5445770, 127.0559740, NOW(), NOW(), 1, '성수 어니언 팩토리', '직접 구운 유기농 빵과 스페셜티 커피가 있는 곳입니다.', '{"mon": "09:00-22:00", "tue": "09:00-22:00", "wed": "09:00-22:00", "thu": "09:00-22:00", "fri": "09:00-23:00", "sat": "10:00-23:00", "sun": "10:00-22:00"}', '성동구 성수동2가 277-7', '서울특별시 성동구 성수동2가 277-7', '서울특별시 성동구 아차산로9길 8', - 'onion_ss@example.com', 'BAKERY' -); - --- 2번 매장: 강남역 조용한 작업용 카페 -( - 37.4979420, 127.0276210, NOW(), NOW(), - 2, '코드앤커피 강남점', '조용한 분위기에서 코딩과 공부를 즐길 수 있는 IT 테마 카페입니다.', - '{"mon": "08:00-23:00", "tue": "08:00-23:00", "wed": "08:00-23:00", "thu": "08:00-23:00", "fri": "08:00-24:00", "sat": "09:00-24:00", "sun": "09:00-22:00"}', + 'onion_ss@test.com', 'BAKERY' +), +-- 그룹 2: 강남 (매니저 2) +(37.4979420, 127.0276210, NOW(), NOW(), + 2, '코드앤커피 강남점', '개발자를 위한 조용한 작업 공간과 진한 에스프레소.', + '{"mon": "08:00-23:00", "tue": "08:00-23:00", "wed": "08:00-23:00", "thu": "08:00-23:00", "fri": "08:00-23:00", "sat": "09:00-22:00", "sun": "09:00-22:00"}', '강남구 역삼동 820-1', '서울특별시 강남구 역삼동 820-1', '서울특별시 강남구 테헤란로 1길 10', - 'code_coffee@example.com', 'CAFE' + 'code_coffee@test.com', 'CAFE' ), --- 3번 매장: 연남동 작은 디저트 샵 -( - 37.5613340, 126.9230550, NOW(), NOW(), - 3, '연남 달콤상점', '매일 아침 직접 구운 마카롱과 수제 티를 판매합니다.', +-- 그룹 3: 연남 (매니저 3) +(37.5613340, 126.9230550, NOW(), NOW(), + 3, '연남 달콤상점', '매일 아침 직접 구운 마카롱과 수제 티 전문점.', '{"mon": "closed", "tue": "12:00-20:00", "wed": "12:00-20:00", "thu": "12:00-20:00", "fri": "12:00-21:00", "sat": "11:00-21:00", "sun": "11:00-20:00"}', '마포구 연남동 255-2', '서울특별시 마포구 연남동 255-2', '서울특별시 마포구 성미산로 190', - 'yeonnam_sweet@example.com', 'DESSERT' + 'yeonnam_sweet@test.com', 'BAKERY' +), +-- 그룹 4: 이태원 (매니저 4) +(37.5348330, 126.9926440, NOW(), NOW(), + 4, '글로벌 키친 이태원', '전 세계의 다양한 가정식을 한곳에서 맛볼 수 있는 레스토랑.', + '{"mon": "11:00-21:00", "tue": "11:00-21:00", "wed": "11:00-21:00", "thu": "11:00-21:00", "fri": "11:00-23:00", "sat": "11:00-23:00", "sun": "11:00-21:00"}', + '용산구 이태원동 123-5', '서울특별시 용산구 이태원동 123-5', '서울특별시 용산구 이태원로 150', + 'global_k@test.com', 'RESTAURANT' +), + +-- 그룹 5: 판교 (매니저 5) +(37.4020050, 127.1089180, NOW(), NOW(), + 5, '판교 테크 베이커리', '바쁜 직장인들을 위한 영양 가득한 샌드위치와 베이커리.', + '{"mon": "07:30-19:00", "tue": "07:30-19:00", "wed": "07:30-19:00", "thu": "07:30-19:00", "fri": "07:30-19:00", "sat": "closed", "sun": "closed"}', + '분당구 삼평동 670', '경기도 성남시 분당구 삼평동 670', '경기도 성남시 분당구 판교역로 231', + 'pangyo_b@test.com', 'BAKERY' +), + +(37.5407, 127.0700, NOW(), NOW(), + 6, '건대 맛도리 박스', '대학가 가성비 최고의 도시락 랜덤박스!', + '{"mon": "10:00-21:00", "tue": "10:00-21:00", "wed": "10:00-21:00", "thu": "10:00-21:00", "fri": "10:00-21:00", "sat": "11:00-20:00", "sun": "closed"}', + '광진구 화양동 7-3', '서울특별시 광진구 화양동 7-3', '서울특별시 광진구 능동로 120', + 'ku_box@test.com', 'RESTAURANT'), + +-- 7. 왕십리 (조회 대상: 약 2.1km) +(37.5612, 127.0384, NOW(), NOW(), + 7, '왕십리 엔터박스', '엔터식스 쇼핑 후 즐기는 시원한 커피와 디저트 세트.', + '{"mon": "10:30-22:00", "tue": "10:30-22:00", "wed": "10:30-22:00", "thu": "10:30-22:00", "fri": "10:30-22:00", "sat": "10:30-22:00", "sun": "10:30-22:00"}', + '성동구 행당동 155-1', '서울특별시 성동구 행당동 155-1', '서울특별시 성동구 왕십리광장로 17', + 'ws_enter@test.com', 'CAFE'), + +-- 8. 잠실 (제외 대상: 약 4.2km) +(37.5133, 127.1001, NOW(), NOW(), + 8, '잠실 롯데 타워 박스', '화려한 롯데타워 뷰와 함께 즐기는 프리미엄 디저트.', + '{"mon": "10:30-20:00", "tue": "10:30-20:00", "wed": "10:30-20:00", "thu": "10:30-20:00", "fri": "10:30-20:30", "sat": "10:30-20:30", "sun": "10:30-20:30"}', + '송파구 신천동 29', '서울특별시 송파구 신천동 29', '서울특별시 송파구 올림픽로 300', + 'jamsil_l@test.com', 'BAKERY'), + +-- 9. 서울숲 (조회 대상: 약 0.9km) +(37.5443, 127.0440, NOW(), NOW(), + 9, '서울숲 피크닉 세트', '서울숲 나들이 갈 때 딱 좋은 샌드위치 박스입니다.', + '{"mon": "09:00-18:00", "tue": "09:00-18:00", "wed": "09:00-18:00", "thu": "09:00-18:00", "fri": "09:00-18:00", "sat": "08:00-19:00", "sun": "08:00-19:00"}', + '성동구 성수동1가 685', '서울특별시 성동구 성수동1가 685', '서울특별시 성동구 뚝섬로 273', + 'forest_p@test.com', 'BAKERY'), + +-- 10. 한양대 (조회 대상: 약 1.8km) +(37.5555, 127.0436, NOW(), NOW(), + 10, '한양대 공대생 간식', '밤샘 공부엔 역시 든든한 야식 랜덤박스.', + '{"mon": "11:00-02:00", "tue": "11:00-02:00", "wed": "11:00-02:00", "thu": "11:00-02:00", "fri": "11:00-02:00", "sat": "11:00-24:00", "sun": "closed"}', + '성동구 사근동 110', '서울특별시 성동구 사근동 110', '서울특별시 성동구 사근동길 2', + 'hanyang_s@test.com', 'BAKERY'), + +-- 11. 압구정 (조회 대상: 약 2.7km) +(37.5270, 127.0284, NOW(), NOW(), + 11, '압구정 명품 랜덤박스', '압구정 로데오의 감성을 담은 세련된 요리 모음.', + '{"mon": "12:00-22:00", "tue": "12:00-22:00", "wed": "12:00-22:00", "thu": "12:00-22:00", "fri": "12:00-24:00", "sat": "12:00-24:00", "sun": "12:00-21:00"}', + '강남구 압구정동 369-1', '서울특별시 강남구 압구정동 369-1', '서울특별시 강남구 압구정로 201', + 'apgu_m@test.com', 'RESTAURANT'), + +-- 12. 논현 (제외 대상: 약 3.8km) +(37.5111, 127.0215, NOW(), NOW(), + 12, '논현동 심야 상자', '야근하는 직장인을 위한 스트레스 해소 매운맛 박스.', + '{"mon": "18:00-05:00", "tue": "18:00-05:00", "wed": "18:00-05:00", "thu": "18:00-05:00", "fri": "18:00-06:00", "sat": "18:00-06:00", "sun": "18:00-03:00"}', + '강남구 논현동 142-2', '서울특별시 강남구 논현동 142-2', '서울특별시 강남구 학동로 2길 15', + 'nonhyeon_s@test.com', 'RESTAURANT'), + +-- 13. 청담 (조회 대상: 약 2.4km) +(37.5191, 127.0519, NOW(), NOW(), + 13, '청담 고급 디저트', '프랑스 파티시에가 직접 만든 고품격 구움과자.', + '{"mon": "11:00-20:00", "tue": "11:00-20:00", "wed": "11:00-20:00", "thu": "11:00-20:00", "fri": "11:00-20:00", "sat": "11:00-19:00", "sun": "closed"}', + '강남구 청담동 80-1', '서울특별시 강남구 청담동 80-1', '서울특별시 강남구 선릉로 152길 5', + 'cheongdam_d@test.com', 'BAKERY'), + +-- 14. 천호 (제외 대상: 약 6.5km) +(37.5385, 127.1235, NOW(), NOW(), + 14, '천호 쭈꾸미 박스', '천호동 쭈꾸미 골목의 화끈한 맛을 집에서 즐기세요.', + '{"mon": "11:30-23:00", "tue": "11:30-23:00", "wed": "11:30-23:00", "thu": "11:30-23:00", "fri": "11:30-24:00", "sat": "11:30-24:00", "sun": "11:30-22:30"}', + '강동구 천호동 411-5', '서울특별시 강동구 천호동 411-5', '서울특별시 강동구 천호대로 1005', + 'cheonho_j@test.com', 'RESTAURANT'), + +-- 15. 군자 (조회 대상: 약 2.2km) +(37.5572, 127.0795, NOW(), NOW(), + 15, '군자역 가성비 박스', '퇴근길에 쓱! 가격 파괴 미친 구성 랜덤박스.', + '{"mon": "08:00-23:00", "tue": "08:00-23:00", "wed": "08:00-23:00", "thu": "08:00-23:00", "fri": "08:00-23:00", "sat": "09:00-22:00", "sun": "09:00-21:00"}', + '광진구 군자동 48-11', '서울특별시 광진구 군자동 48-11', '서울특별시 광진구 군자로 101', + 'gunja_v@test.com', 'BAKERY'), + +(37.5472, 127.0474, NOW(), NOW(), + 16, '뚝섬 블루 카페', '스페셜티 커피와 조용한 분위기의 카페입니다.', + '{"mon": "08:00-22:00", "tue": "08:00-22:00", "wed": "08:00-22:00", "thu": "08:00-22:00", "fri": "08:00-23:00", "sat": "09:00-23:00", "sun": "09:00-22:00"}', + '성동구 성수동1가 13-167', '서울특별시 성동구 성수동1가 13-167', '서울특별시 성동구 상원길 15', + 'ttuk_cafe@test.com', 'CAFE'), + +-- 17. 자양 (약 1.5km, 조회됨) +(37.5340, 127.0680, NOW(), NOW(), + 17, '자양동 소금빵집', '갓 구운 소금빵이 맛있는 동네 베이커리.', + '{"mon": "09:00-20:00", "tue": "09:00-20:00", "wed": "09:00-20:00", "thu": "09:00-20:00", "fri": "09:00-21:00", "sat": "09:00-21:00", "sun": "closed"}', + '광진구 자양동 606', '서울특별시 광진구 자양동 606', '서울특별시 광진구 자양로 50', + 'jayang_b@test.com', 'BAKERY'), + +-- 18. 구의 (약 2.5km, 조회됨) +(37.5460, 127.0850, NOW(), NOW(), + 18, '구의역 파스타 하우스', '정통 이탈리안 파스타를 가성비 있게 즐기세요.', + '{"mon": "11:00-22:00", "tue": "11:00-22:00", "wed": "11:00-22:00", "thu": "11:00-22:00", "fri": "11:00-22:00", "sat": "11:30-22:00", "sun": "11:30-21:00"}', + '광진구 구의동 252', '서울특별시 광진구 구의동 252', '서울특별시 광진구 아차산로 380', + 'guui_rest@test.com', 'RESTAURANT'), + +-- 19. 화양 (약 1.8km, 조회됨) +(37.5500, 127.0750, NOW(), NOW(), + 19, '화양동 디저트 빌리지', '다양한 조각 케이크가 가득한 디저트 카페.', + '{"mon": "12:00-23:00", "tue": "12:00-23:00", "wed": "12:00-23:00", "thu": "12:00-23:00", "fri": "12:00-24:00", "sat": "12:00-24:00", "sun": "12:00-22:00"}', + '광진구 화양동 111', '서울특별시 광진구 화양동 111', '서울특별시 광진구 군자로 15', + 'hwayang_c@test.com', 'CAFE'), + +-- 20. 마장 (약 2.6km, 조회됨) +(37.5660, 127.0450, NOW(), NOW(), + 20, '마장 스테이크 하우스', '질 좋은 고기를 합리적인 가격에 제공합니다.', + '{"mon": "11:00-23:00", "tue": "11:00-23:00", "wed": "11:00-23:00", "thu": "11:00-23:00", "fri": "11:00-23:00", "sat": "10:00-23:00", "sun": "10:00-22:00"}', + '성동구 마장동 510', '서울특별시 성동구 마장동 510', '서울특별시 성동구 살곶이길 18', + 'majang_r@test.com', 'RESTAURANT'), + +-- 21. 장안 (약 2.0km, 조회됨) +(37.5610, 127.0650, NOW(), NOW(), + 21, '장안동 깜빠뉴', '천연 발효종을 사용하는 건강한 빵집.', + '{"mon": "08:00-21:00", "tue": "08:00-21:00", "wed": "08:00-21:00", "thu": "08:00-21:00", "fri": "08:00-21:00", "sat": "08:00-20:00", "sun": "closed"}', + '동대문구 장안동 370', '서울특별시 동대문구 장안동 370', '서울특별시 동대문구 장한로 10', + 'jangan_b@test.com', 'BAKERY'), + +-- 22. 건대입구 (약 1.3km, 조회됨) +(37.5407, 127.0700, NOW(), NOW(), + 22, '건대 라운지 카페', '대학가 풍경이 한눈에 들어오는 뷰 맛집 카페.', + '{"mon": "10:00-23:00", "tue": "10:00-23:00", "wed": "10:00-23:00", "thu": "10:00-23:00", "fri": "10:00-24:00", "sat": "10:00-24:00", "sun": "10:00-22:00"}', + '광진구 화양동 5', '서울특별시 광진구 화양동 5', '서울특별시 광진구 능동로 110', + 'konkuk_c@test.com', 'CAFE'), + +-- 23. 삼성 (약 3.5km, 제외 대상) +(37.5150, 127.0400, NOW(), NOW(), + 23, '코엑스 브런치', '도심 속 여유를 즐기는 브런치 레스토랑.', + '{"mon": "09:30-20:00", "tue": "09:30-20:00", "wed": "09:30-20:00", "thu": "09:30-20:00", "fri": "09:30-21:00", "sat": "10:00-21:00", "sun": "10:00-20:00"}', + '강남구 삼성동 159', '서울특별시 강남구 삼성동 159', '서울특별시 강남구 영동대로 513', + 'coex_r@test.com', 'RESTAURANT'), + +-- 24. 회기 (약 4.0km, 제외 대상) +(37.5800, 127.0600, NOW(), NOW(), + 24, '경희대 앞 빵집', '추억이 깃든 대학가 오래된 베이커리.', + '{"mon": "08:00-22:00", "tue": "08:00-22:00", "wed": "08:00-22:00", "thu": "08:00-22:00", "fri": "08:00-22:00", "sat": "09:00-21:00", "sun": "09:00-21:00"}', + '동대문구 회기동 1', '서울특별시 동대문구 회기동 1', '서울특별시 동대문구 경희대로 26', + 'hoegi_b@test.com', 'BAKERY'), + +-- 25. 가락 (약 6.0km, 제외 대상) +(37.5000, 127.1000, NOW(), NOW(), + 25, '가락시장 신선 카페', '시장 근처 신선한 과일 주스가 유명한 카페.', + '{"mon": "06:00-18:00", "tue": "06:00-18:00", "wed": "06:00-18:00", "thu": "06:00-18:00", "fri": "06:00-18:00", "sat": "06:00-15:00", "sun": "closed"}', + '송파구 가락동 600', '서울특별시 송파구 가락동 600', '서울특별시 송파구 양재대로 932', + 'garak_c@test.com', 'CAFE'), + +(37.4840, 126.8115, NOW(), NOW(), + 26, '역곡역 감성 카페', '역곡역 인근 카공족을 위한 조용한 명소입니다.', + '{"mon": "10:00-22:00", "tue": "10:00-22:00", "wed": "10:00-22:00", "thu": "10:00-22:00", "fri": "10:00-23:00", "sat": "11:00-23:00", "sun": "11:00-21:00"}', + '부천시 역곡동 74', '경기도 부천시 역곡동 74', '경기도 부천시 경인로 505', + 'yg_cafe@test.com', 'CAFE' +), +(37.4875, 126.7915, NOW(), NOW(), + 27, '소사 마스터 베이커리', '매일 아침 8시에 갓 구운 식빵이 나옵니다.', + '{"mon": "08:00-20:00", "tue": "08:00-20:00", "wed": "08:00-20:00", "thu": "08:00-20:00", "fri": "08:00-21:00", "sat": "09:00-18:00", "sun": "closed"}', + '부천시 소사본동 65', '경기도 부천시 소사본동 65', '경기도 부천시 소사로 170', + 'sosa_b@test.com', 'BAKERY' +), +(37.4915, 126.8235, NOW(), NOW(), + 28, '온수역 파스타 전문점', '온수역 근처 숨은 양식 맛집, 생면 파스타 전문.', + '{"mon": "11:30-21:00", "tue": "11:30-21:00", "wed": "11:30-21:00", "thu": "11:30-21:00", "fri": "11:30-22:00", "sat": "12:00-21:00", "sun": "12:00-20:00", "break": "15:00-17:00"}', + '구로구 온수동 45', '서울특별시 구로구 온수동 45', '서울특별시 구로구 경인로 3길 10', + 'onsu_r@test.com', 'RESTAURANT' +), +(37.5035, 126.7975, NOW(), NOW(), + 29, '부천 종합운동장 카페', '공원 산책로와 연결된 탁 트인 뷰를 가진 카페.', + '{"mon": "09:00-19:00", "tue": "09:00-19:00", "wed": "09:00-19:00", "thu": "09:00-19:00", "fri": "09:00-19:00", "sat": "09:00-21:00", "sun": "09:00-21:00"}', + '부천시 춘의동 8', '경기도 부천시 춘의동 8', '경기도 부천시 길주로 486', + 'stadium_c@test.com', 'CAFE' +), +(37.4955, 126.8315, NOW(), NOW(), + 30, '궁동 베이커리 하우스', '천연 발효종을 사용한 건강한 호밀빵 전문점.', + '{"mon": "09:00-21:00", "tue": "09:00-21:00", "wed": "09:00-21:00", "thu": "09:00-21:00", "fri": "09:00-21:00", "sat": "10:00-18:00", "sun": "closed"}', + '구로구 궁동 213', '서울특별시 구로구 궁동 213', '서울특별시 구로구 오리로 21길 5', + 'gung_b@test.com', 'BAKERY' +), + +-- 3~4km 내외 (경계선) +(37.4725, 126.8125, NOW(), NOW(), + 31, '범박동 브런치 카페', '조용한 주택가에서 즐기는 여유로운 브런치.', + '{"mon": "10:00-18:00", "tue": "10:00-18:00", "wed": "closed", "thu": "10:00-18:00", "fri": "10:00-18:00", "sat": "10:00-20:00", "sun": "10:00-20:00"}', + '부천시 범박동 155', '경기도 부천시 범박동 155', '경기도 부천시 양지로 20', + 'beom_c@test.com', 'CAFE' +), +(37.4945, 126.8555, NOW(), NOW(), + 32, '개봉동 돈까스 팩토리', '한정 수량으로 판매하는 수제 돈까스 맛집.', + '{"mon": "11:00-21:00", "tue": "11:00-21:00", "wed": "11:00-21:00", "thu": "11:00-21:00", "fri": "11:00-21:00", "sat": "11:00-20:00", "sun": "closed", "break": "15:00-16:30"}', + '구로구 개봉동 170', '서울특별시 구로구 개봉동 170', '서울특별시 구로구 개봉로 15', + 'gb_r@test.com', 'RESTAURANT' +), +(37.4855, 126.8375, NOW(), NOW(), + 33, '천왕역 로컬 베이커리', '동네 주민들이 즐겨 찾는 아기자기한 빵집.', + '{"mon": "07:30-22:00", "tue": "07:30-22:00", "wed": "07:30-22:00", "thu": "07:30-22:00", "fri": "07:30-22:00", "sat": "08:00-21:00", "sun": "08:00-21:00"}', + '구로구 천왕동 12', '서울특별시 구로구 천왕동 12', '서울특별시 구로구 천왕로 10', + 'cw_b@test.com', 'BAKERY' +), +(37.4925, 126.7655, NOW(), NOW(), + 34, '부천 중동 이탈리안', '기념일에 오기 좋은 고급스러운 이탈리안 레스토랑.', + '{"mon": "12:00-22:00", "tue": "12:00-22:00", "wed": "12:00-22:00", "thu": "12:00-22:00", "fri": "12:00-23:00", "sat": "11:00-23:00", "sun": "11:00-21:00", "break": "15:30-17:30"}', + '부천시 중동 1141', '경기도 부천시 중동 1141', '경기도 부천시 길주로 180', + 'jd_r@test.com', 'RESTAURANT' +), +(37.4795, 126.8555, NOW(), NOW(), + 35, '광명사거리 카페', '광명사거리역 인근 로스터리 핸드드립 전문점.', + '{"mon": "10:00-22:00", "tue": "10:00-22:00", "wed": "10:00-22:00", "thu": "10:00-22:00", "fri": "10:00-22:00", "sat": "11:00-22:00", "sun": "11:00-21:00"}', + '광명시 광명동 158', '경기도 광명시 광명동 158', '경기도 광명시 오리로 950', + 'gm_c@test.com', 'CAFE' +), + +-- 5~6km 내외 (멀음) +(37.4945, 126.8455, NOW(), NOW(), + 36, '오류동 스테이크', '참나무 장작으로 구운 불맛 스테이크 하우스.', + '{"mon": "closed", "tue": "17:00-23:00", "wed": "17:00-23:00", "thu": "17:00-23:00", "fri": "17:00-23:00", "sat": "12:00-23:00", "sun": "12:00-22:00"}', + '구로구 오류동 135', '서울특별시 구로구 오류동 135', '서울특별시 구로구 경인로 190', + 'oryu_r@test.com', 'RESTAURANT' +), +(37.5005, 126.8655, NOW(), NOW(), + 37, '고척 스카이 베이커리', '고척돔 경기 날 필수 방문 코스, 마카롱 전문.', + '{"mon": "10:00-20:00", "tue": "10:00-20:00", "wed": "10:00-20:00", "thu": "10:00-20:00", "fri": "10:00-21:00", "sat": "10:00-21:00", "sun": "10:00-19:00"}', + '구로구 고척동 76', '서울특별시 구로구 고척동 76', '서울특별시 구로구 경인로 430', + 'gocheok_b@test.com', 'BAKERY' +), +(37.4765, 126.8675, NOW(), NOW(), + 38, '철산역 만남 카페', '대규모 좌석과 카공 테라스가 있는 대형 카페.', + '{"mon": "08:00-24:00", "tue": "08:00-24:00", "wed": "08:00-24:00", "thu": "08:00-24:00", "fri": "08:00-01:00", "sat": "08:00-01:00", "sun": "09:00-23:00"}', + '광명시 철산동 241', '경기도 광명시 철산동 241', '경기도 광명시 철산로 20', + 'cheol_c@test.com', 'CAFE' +), +(37.4685, 126.8285, NOW(), NOW(), + 39, '옥길동 파스타', '옥길 지구 맘카페 소문난 패밀리 레스토랑.', + '{"mon": "11:30-21:30", "tue": "11:30-21:30", "wed": "11:30-21:30", "thu": "11:30-21:30", "fri": "11:30-21:30", "sat": "11:00-21:30", "sun": "11:00-21:00", "break": "15:00-17:00"}', + '부천시 옥길동 710', '경기도 부천시 옥길동 710', '경기도 부천시 옥길로 110', + 'ok_r@test.com', 'RESTAURANT' +), +(37.5035, 126.7635, NOW(), NOW(), + 40, '부천시청 맛집 상자', '부천시청 직장인들이 애용하는 덮밥 도시락.', + '{"mon": "11:00-23:00", "tue": "11:00-23:00", "wed": "11:00-23:00", "thu": "11:00-23:00", "fri": "11:00-23:00", "sat": "11:00-24:00", "sun": "closed"}', + '부천시 중동 1156', '경기도 부천시 중동 1156', '경기도 부천시 소향로 181', + 'bc_r@test.com', 'RESTAURANT' ); \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 627ee36..088e705 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -106,6 +106,13 @@ kakao: base-url: https://dapi.kakao.com rest-api-key: ${KAKAO_REST_API_KEY} +kakaopay: + cid: TC0ONETIME + secret-key: ${KAKAOPAY_SECRET_KEY_DEV} + base-url: https://open-api.kakaopay.com + approval-url: https://compasser.co.kr/payments/kakaopay/success + cancel-url: https://compasser.co.kr/payments/kakaopay/cancel + fail-url: https://compasser.co.kr/payments/kakaopay/fail springdoc: packages-to-scan: Comprehensive_Design_Project.CUK_Compasser \ No newline at end of file