From ed3eabd85c6b5cae6ef0a2a78a80c4f8d8ddfc73 Mon Sep 17 00:00:00 2001 From: BOMIN LYU <83059096+rbm0524@users.noreply.github.com> Date: Fri, 1 Nov 2024 21:56:41 +0900 Subject: [PATCH 1/3] =?UTF-8?q?9=EC=A3=BC=EC=B0=A8=20Develop=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20(#68)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: SimpleJpaXXXRepository 추가 * feat: JpaProductRepository 추가 * feat: JpaPaymentOrderRepository 추가 * feat: JpaPaymentEventRepository 추가 * feat: 멱등성 키 생성기 구현 * feat: 결제 준비 DTO 구현 * feat: 결제 주문 응답 DTO 구현 * feat: 결제 준비 기능 구현 * test: PaymentDatabaseHelper 추가 * test: JpaDatabaseCleanup 추가 * test: JpaPaymentDatabaseHelper 추가 * test: test 설정 파일 추가 * test: 결제 준비 기능 테스트 추가 - 결제 준비에 대한 정상 케이스 - 중복 결제 준비 요청에 대한 예외 발생 케이스 * feat: application.yml data.sql 수행 설정 * feat: data.sql 데이터 추가 * chore: build.gradle 스프링 시큐리티 의존성 제거 * feat: 결제 Repository 빈 등록 * feat: PaymentController 결제 준비 엔드포인트 구현 * feat: Api 응답 본문 구조 선언 * feat: 결제 주문 엔티티와 상품 연관관계 삭제 * feat: 결제 상태에 description 필드 추가 * feat: Entity 구현 * feat: Domain 구현 * feat: Mapper 구현 * feat: ProductRepository 추가 * feat: PaymentOrderRepository 추가 * feat: PaymentEventRepository 추가 * feat: SimpleJpaXXXRepository 추가 * feat: JpaProductRepository 추가 * feat: JpaPaymentOrderRepository 추가 * feat: JpaPaymentEventRepository 추가 * feat: 멱등성 키 생성기 구현 * feat: 결제 준비 DTO 구현 * feat: 결제 주문 응답 DTO 구현 * feat: 결제 준비 기능 구현 * test: PaymentDatabaseHelper 추가 * test: JpaDatabaseCleanup 추가 * test: JpaPaymentDatabaseHelper 추가 * test: test 설정 파일 추가 * test: 결제 준비 기능 테스트 추가 - 결제 준비에 대한 정상 케이스 - 중복 결제 준비 요청에 대한 예외 발생 케이스 * feat: application.yml data.sql 수행 설정 * feat: data.sql 데이터 추가 * feat: 결제 Repository 빈 등록 * feat: PaymentController 결제 준비 엔드포인트 구현 * feat: Api 응답 본문 구조 선언 * style: 파일 끝 빈 줄 추가 * feat: DTO record로 변경 * rename: package 이름 수정 memebr -> member * style: 오타 수정 * rename: 오타 수정 * chore: spring security 의존성 제거 사용하지 않음 * style: 코드 린트 적용 * style: 코드 린트 적용 * feat: application-test.yml 누락된 설정 추가 * feat: 애플리케이션 컨텍스트 로드 테스트 test 프로파일 활성화 * test: 결제 준비 테스트 유지보수 DTO 가 record 타입으로 변경됨에 따른 수정 * feat: 주문 상세 정보 생성 dto 작성 * feat: OrderDetailServcie 생성 주문 상세 정보 생성 메서드만 작성 추후 RUD도 제작 예정 * feat: OrderDetailController 생성 우선적으로 주문 생성만 만들어둠 * fix: member패키지 오타 수정 적용 및 spot 엔티티 필드 명 변경 적용 * feat: swagger config 작성 * feat: swagger 의존성 추가 * style: spotless 적용 * chore: build.gradle 버전 변수 추출 및 spotless 설정 변경 spotless 들여쓰기가 공백 2자에서 4자로 적용되도록 수정 * feat: BaseEntity 추가 * feat: AuditorProvider 구현 * feat: 영속성 설정 클래스 추가 * feat: 애플리케이션 실행 클래스 @EnableJpaAuditing 제거 PersistenceConfig 에서 적용되어 중복 제거 * feat: 카카오 로그인 구현 * refactor: 인텔리제이 마지막 빈 줄 설정 옵션 적용 * refactor: 불필요한 어노테이션 제거 * refactor: RestClient Bean으로 등록 후 Autowired 적용 * refactor: 구체적인 버전은 따로 변수로 추출해서 한 곳에서 관리 * refactor: 불필요한 어노테이션 제거 Json 필드 이름과 KakaoAccount 객체의 필드 이름이 같음 * refactor: @RequiredArgsConstructor 적용 생성자 생략 가능 * refactor: 정적 팩토리 메서드 네이밍 방식 반영 * feat: swagger config 작성 * feat: swagger 의존성 추가 * feat: Soft delete를 위해 isDeleted 속성 추가 * refactor: isDeleted가 false인 것만 조회하도록 수정 * refactor: isDeleted가 false인 것만 조회하도록 수정 * refactor: isDeleted 추가 * refactor: BaseEntity 상속하도록 수정 * refactor: createdAt, updateAt 필드 추가 * feat: ENUM 타입의 category 추가 * refactor: category의 타입을 Category(ENUM 클래스)로 변경 * feat: ENUM 클래스의 converter에 필요한 CodedEnum 인터페이스 작성 * feat: ENUM 클래스의 converter 작성 * style: 코드 컨벤션 수정 * style: camel case로 변경 * refactor: setter로 변경하던 isDeleted를 delete와 restore 메서드로 변경 * refactor: dirty checking하므로 save가 필요 없어서 삭제 * fix: Converter 어노테이션 붙임 * feat: 결제 주문 엔티티와 상품 연관관계 삭제 * feat: 결제 상태에 description 필드 추가 * feat: Entity 구현 * feat: Domain 구현 * feat: Mapper 구현 * feat: ProductRepository 추가 * feat: PaymentOrderRepository 추가 * feat: PaymentEventRepository 추가 * feat: SimpleJpaXXXRepository 추가 * feat: JpaProductRepository 추가 * feat: JpaPaymentOrderRepository 추가 * feat: JpaPaymentEventRepository 추가 * feat: 멱등성 키 생성기 구현 * feat: 결제 준비 DTO 구현 * feat: 결제 주문 응답 DTO 구현 * feat: 결제 준비 기능 구현 * test: PaymentDatabaseHelper 추가 * test: JpaDatabaseCleanup 추가 * test: JpaPaymentDatabaseHelper 추가 * test: test 설정 파일 추가 * test: 결제 준비 기능 테스트 추가 - 결제 준비에 대한 정상 케이스 - 중복 결제 준비 요청에 대한 예외 발생 케이스 * feat: application.yml data.sql 수행 설정 * feat: data.sql 데이터 추가 * feat: 결제 Repository 빈 등록 * feat: PaymentController 결제 준비 엔드포인트 구현 * feat: Api 응답 본문 구조 선언 * style: 파일 끝 빈 줄 추가 * feat: DTO record로 변경 * rename: package 이름 수정 memebr -> member * style: 오타 수정 * rename: 오타 수정 * style: 코드 린트 적용 * style: 코드 린트 적용 * feat: application-test.yml 누락된 설정 추가 * feat: 애플리케이션 컨텍스트 로드 테스트 test 프로파일 활성화 * test: 결제 준비 테스트 유지보수 DTO 가 record 타입으로 변경됨에 따른 수정 * fix: OrderDetail 엔티티 수정 팀원들과 회의 후 해당 엔티티 구조를 수정하였고, OrderParticipant는 필요가 없다고 판단 * remove: OrderParticipant 삭제 * remove: OrderParticipant 레포지토리 삭제 * refactor: OrderDetail 수정된 엔티티에 맞게 리팩토링 * fix: 충돌 해결 * feat: PaymentOrderRepository 결제 최종 금액 계산 구현 * refactor: JpaPaymentOrderRepository 조회 실패 시, 발생할 예외 구체화 IllegalArgumentException -> NoSuchElementException * feat: 결제 유효성 검사 구현 * style: PaymentValidationServiceTest 오타 수정 * feat: Mapstruct 사용을 위한 의존성 추가 * feat: Controller 계층 DTO 작성 * feat: Mapstruct를 이용해 SpotMapper 생성 * refactor: 이름 수정 * feat: SimpleSpotRepository를 가지는 Repository 계층 생성 * refactor: Controller 계층의 Dto를 반환하도록 수정, Service 계층의 Dto를 인자로 넘기도록 수정 * refactor: servicedto 패키지로 이동, accessLevel 조정 * refactor: 매핑 방식 변경 * feat: Member의 PK를 참조하도록 수정 * refactor: 조회 메서드 readOnly로 변경 * style: spotless 적용 # Conflicts: # src/main/java/com/ordertogether/team14_be/memebr/persistence/entity/Member.java # src/main/java/com/ordertogether/team14_be/payment/domain/PaymentEvent.java # src/main/java/com/ordertogether/team14_be/payment/domain/PaymentOrder.java # src/main/java/com/ordertogether/team14_be/payment/domain/PaymentOrderStatus.java # src/main/java/com/ordertogether/team14_be/payment/domain/Product.java # src/main/java/com/ordertogether/team14_be/spot/controller/SpotController.java # src/main/java/com/ordertogether/team14_be/spot/dto/SpotDto.java # src/main/java/com/ordertogether/team14_be/spot/entity/Spot.java # src/main/java/com/ordertogether/team14_be/spot/repository/SpotRepository.java # src/main/java/com/ordertogether/team14_be/spot/service/SpotService.java * feat: 카카오 로그인 구현 * refactor: @RequiredArgsConstructor 적용 생성자 생략 가능 * feat: PaymentOrder 와 PaymentEvent 연관관계 추가 * feat: 결제 상태에 description 필드 추가 * feat: Domain 구현 * rename: package 이름 수정 memebr -> member * style: 전역 상수변수 같은 경우에는 변수명을 대문자로 사용 * refactor: jwt에 사용자 정보를 담지 않음 email 대신 사용자의 아이디(pk)를 담음 * refactor: 생성자 주입방식 하나로 통일, @Autowired 지양 * refactor: Enum의 이름을 그대로 반환하도록 수정 * feat: 반경 n미터 내 Spot 조회하기 작성 * feat: 반경 n미터 내 Spot들만 처리해서 반환하는 메서드 작성 * feat: 최대/최소 경도, 위도 내에 해당하는 Spot을 반환하는 메서드 작성 * feat: 최대/최소 경도, 위도 내에 해당하는 Spot을 반환하는 쿼리 작성 * feat: SpotModifyRequest와 SpotDto를 매핑하는 메서드 추가 * refactor: NotNull로 Null 방지 * feat: ErrorResponse 정의 * feat: ErrorCode 정의 * feat: id에 해당하는 Spot이 없는 경우 Exception 정의 * feat: SpotExceptionHandler 작성 * refactor: JwtUtil Spring Bean 제거 * refactor: util 클래스는 상속이 불가능하게 final 설정 * refactor: 토큰 재료를 일반적인 파라미터명으로 변경 * refactor: db 관련 작업 트랜잭션 추가 * refactor: EXPIRE_TIME은 �애플리케이션 설정(application.yaml)으로 변경 * refactor: 카카오 관련 요소 한 곳(KakaoProperties)에서 관리 * refactor: jwt를 사용자 아이디를 가지고 생성 * fix: 프로그램 수행 가능하도록 함 * 7주차 weekly 병합 (#56) * fix: 프로그램 수행 가능하도록 함 * fix: 안 올라간 파일.. * feat: 토큰 해독 메서드 구현 * feat: 로그인 어노테이션 구현 * feat: 멤버 조회 수정 삭제 구현 * feat: 회원 추가정보 등록 후 회원가입 * fix: 로그인 에러 해결 * style: 안 쓰는 메서드 및 주석 제거, 파일 깔끔하게! * refactor: 회원 수정 db 재저장 * test: 회원 API 테스트코드 작성 * feat: 결제 파트 누락된 파일 추가 * refactor: 트랜잭션 변경 및 readOnly 적용 * feat: 전역에러핸들러 작성 * refactor: 클라이언트는 멤버아이디를 모른다.. 토큰 사용.. * refactor: 매직리터럴대신 HttpHeaders.AUTHORIZATION 사용 --------- Co-authored-by: westzeroright Co-authored-by: westzeroright <124443419+westzeroright@users.noreply.github.com> * feat: cors 설정 * refactor: Value를 private final로 설정하고 생성자 주입방식을 사용 * refactor: 중복된 메서드 modifyMemberInfo 제거 * refactor: 트랜잭션 추가 * refactor: 메서드명은 행위를 나타내도록 동사로 시작 loginKakaoUser * refactor: esponseEntity, HttpStatus, redirect등을 Controller(Web 계층)로 옮김 * refactor: 사용하지 않은 메서드 및 파일 제거 * feat: 의존성 geohash 추가 * refactor: 반지름 삭제 * refactor: geoHash, deadlineTime 추가 * feat: geoHash를 통해 반경 n미터 spot 조회하는 기능 작성 * feat: geoHash 기준으로 Spot 찾기 추가 * refactor: geoHash, deadlineTime(마감시간) 추가 * refactor: deadlineTime(마감시간) 추가 * fix: 중복 메서드 제거 * refactor: 줄바꿈 변경 * feat: 의존성 geohash 추가 * refactor: 반지름 삭제 * refactor: geoHash, deadlineTime 추가 * feat: geoHash를 통해 반경 n미터 spot 조회하는 기능 작성 * feat: geoHash 기준으로 Spot 찾기 추가 * refactor: geoHash, deadlineTime(마감시간) 추가 * refactor: deadlineTime(마감시간) 추가 * refactor: 줄바꿈 변경 * feat: API 응답 형식 필드 final 키워드 추가 * feat: API 응답 형식 생성 메서드 추가 * feat: 더미 데이터에 회원 데이터 추가 * style: 결제 준비 서비스 검증 메서드 변수명 수정 * feat: PaymentEventEntity paymentKey 필드 @Nullable 제거 결제 승인 요청 전까지 paymentKey 를 알 수 없음 * feat: 결제 승인 프로세스 예시 페이지 구현 * feat: 포인트 충전 기능 구현 * feat: 결제 주문 저장소 빈 등록 시, 주입될 repo 추가 * feat: PaymentEventRepository 결제 상태 변경 기능 구현 * feat: 결제 상태 변경 기능 구현 * test: PaymentDatabaseHelper 더미 데이터 생성 기능 추가 * feat: 결제 유효성 검사 기능 구현 * feat: TossPayments 응답 DTO 생성 - 결제 승인 응답 - 결제 실패 응답 * feat: TossPaymentsClient 구현 * feat: 결제 승인 DTO 구현 * feat: 결제 승인 기능 구현 * test: 테스트 코드 유지보수 - PaymentEvent 최초 저장 시, PaymentKey 값이 null 이되는 것을 반영 * fix: PaymentEventMapper 도메인 변환 시, paymentOrders 값이 채워지지 않던 버그 수정 * feat: PaymentViewController 추가 * feat: application.yml application-test.yml toss 설정 추가 * rename: PointUpdateService -> PointManagementService * feat: 포인트 충전 기능 구현 * feat: 결제 승인 기능 구현 * rename: PointUpdateService -> PointManagementService * feat: 카카오 로그인 구현 * feat: 포인트 충전 기능 구현 --------- Co-authored-by: 나제법 Co-authored-by: 나제법 <89574219+nove1080@users.noreply.github.com> Co-authored-by: ajy9851 Co-authored-by: westzeroright Co-authored-by: westzeroright <124443419+westzeroright@users.noreply.github.com> Co-authored-by: ajy9851 <130203472+ajy9851@users.noreply.github.com> --- build.gradle | 1 + .../ordertogether/team14_be/auth/JwtUtil.java | 1 + .../auth/application/service/AuthService.java | 45 +--- .../application/service/KakaoAuthService.java | 19 ++ .../auth/presentation/AuthController.java | 47 +++- .../common/web/response/ApiResponse.java | 10 +- .../team14_be/config/CorsConfig.java | 19 ++ .../team14_be/config/PersistenceConfig.java | 8 +- .../application/service/MemberService.java | 26 +- .../member/persistence/entity/Member.java | 26 +- .../application/service/MemberService.java | 26 ++ .../memebr/persistence/MemberRepository.java | 12 + .../payment/domain/PaymentStatus.java | 8 + .../jpa/entity/PaymentEventEntity.java | 1 - .../jpa/mapper/PaymentEventMapper.java | 18 +- .../repository/JpaPaymentEventRepository.java | 50 +++- .../repository/JpaPaymentOrderRepository.java | 23 +- .../SimpleJpaPaymentEventRepository.java | 18 ++ .../SimpleJpaPaymentOrderRepository.java | 12 +- .../repository/PaymentEventRepository.java | 16 +- .../repository/PaymentOrderRepository.java | 11 + .../service/PaymentConfirmService.java | 40 +++ .../service/PaymentPreparationService.java | 6 +- .../service/PaymentStatusUpdateService.java | 25 ++ .../service/PaymentValidationService.java | 39 +++ .../service/PointManagementService.java | 31 +++ .../payment/service/PointUpdateService.java | 31 +++ .../command/PaymentStatusUpdateCommand.java | 23 ++ .../web/controller/PaymentController.java | 15 ++ .../web/controller/PaymentViewController.java | 40 +++ .../web/request/PaymentConfirmRequest.java | 6 + .../response/PaymentConfirmationFailure.java | 3 + .../response/PaymentConfirmationResponse.java | 20 ++ .../web/toss/client/TossPaymentsClient.java | 53 ++++ .../toss/config/TossPaymentsClientConfig.java | 30 +++ .../web/toss/error/TossPaymentsError.java | 59 ++++ .../TossPaymentsConfirmationResponse.java | 104 ++++++++ .../spot/controller/SpotController.java | 8 +- .../controllerdto/SpotCreationRequest.java | 4 +- .../controllerdto/SpotCreationResponse.java | 4 +- .../spot/dto/servicedto/SpotDto.java | 3 + .../team14_be/spot/entity/Spot.java | 3 + .../spot/exception/ErrorResponse.java | 2 +- .../spot/repository/SimpleSpotRepository.java | 2 + .../spot/repository/SpotRepository.java | 9 +- .../team14_be/spot/service/SpotService.java | 48 +--- src/main/resources/application.yml | 5 + src/main/resources/data.sql | 2 + src/main/resources/static/style.css | 251 ++++++++++++++++++ src/main/resources/templates/checkout.html | 147 ++++++++++ src/main/resources/templates/fail.html | 34 +++ src/main/resources/templates/success.html | 67 +++++ .../helper/PaymentDatabaseHelper.java | 4 + .../helper/jpa/JpaPaymentDatabaseHelper.java | 75 ++++++ .../service/PaymentConfirmServiceTest.java | 101 +++++++ .../PaymentPreparationServiceTest.java | 2 +- .../PaymentStatusUpdateServiceTest.java | 72 +++++ .../service/PointManagementServiceTest.java | 61 +++++ .../service/PointUpdateServiceTest.java | 62 +++++ src/test/resources/application-test.yml | 12 + 60 files changed, 1766 insertions(+), 134 deletions(-) create mode 100644 src/main/java/com/ordertogether/team14_be/auth/application/service/KakaoAuthService.java create mode 100644 src/main/java/com/ordertogether/team14_be/config/CorsConfig.java create mode 100644 src/main/java/com/ordertogether/team14_be/memebr/application/service/MemberService.java create mode 100644 src/main/java/com/ordertogether/team14_be/memebr/persistence/MemberRepository.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/service/PaymentConfirmService.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/service/PaymentStatusUpdateService.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/service/PaymentValidationService.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/service/PointManagementService.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/service/PointUpdateService.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/service/command/PaymentStatusUpdateCommand.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/web/controller/PaymentViewController.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/web/request/PaymentConfirmRequest.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentConfirmationFailure.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentConfirmationResponse.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/web/toss/client/TossPaymentsClient.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/web/toss/config/TossPaymentsClientConfig.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/web/toss/error/TossPaymentsError.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/web/toss/response/TossPaymentsConfirmationResponse.java create mode 100644 src/main/resources/static/style.css create mode 100644 src/main/resources/templates/checkout.html create mode 100644 src/main/resources/templates/fail.html create mode 100644 src/main/resources/templates/success.html create mode 100644 src/test/java/com/ordertogether/team14_be/payment/service/PaymentConfirmServiceTest.java create mode 100644 src/test/java/com/ordertogether/team14_be/payment/service/PaymentStatusUpdateServiceTest.java create mode 100644 src/test/java/com/ordertogether/team14_be/payment/service/PointManagementServiceTest.java create mode 100644 src/test/java/com/ordertogether/team14_be/payment/service/PointUpdateServiceTest.java diff --git a/build.gradle b/build.gradle index 32b0c650..e0aa23c8 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,7 @@ dependencies { implementation "io.jsonwebtoken:jjwt-api:${jjwt_version}" implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${swagger_version}" implementation 'org.mapstruct:mapstruct:1.6.2' + implementation 'ch.hsr:geohash:1.4.0' annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.2' diff --git a/src/main/java/com/ordertogether/team14_be/auth/JwtUtil.java b/src/main/java/com/ordertogether/team14_be/auth/JwtUtil.java index a16a2c28..deaeb9c7 100644 --- a/src/main/java/com/ordertogether/team14_be/auth/JwtUtil.java +++ b/src/main/java/com/ordertogether/team14_be/auth/JwtUtil.java @@ -11,6 +11,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +@Component public class JwtUtil { private final SecretKey key; private final int expireTime; diff --git a/src/main/java/com/ordertogether/team14_be/auth/application/service/AuthService.java b/src/main/java/com/ordertogether/team14_be/auth/application/service/AuthService.java index b760a7ae..0bca91c1 100644 --- a/src/main/java/com/ordertogether/team14_be/auth/application/service/AuthService.java +++ b/src/main/java/com/ordertogether/team14_be/auth/application/service/AuthService.java @@ -1,56 +1,29 @@ package com.ordertogether.team14_be.auth.application.service; import com.ordertogether.team14_be.auth.JwtUtil; -import com.ordertogether.team14_be.auth.application.dto.KakaoUserInfo; -import com.ordertogether.team14_be.auth.presentation.KakaoClient; -import com.ordertogether.team14_be.common.web.response.ApiResponse; import com.ordertogether.team14_be.member.application.service.MemberService; import com.ordertogether.team14_be.member.persistence.entity.Member; -import java.net.URI; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; -@RequiredArgsConstructor @Service public class AuthService { - - private final KakaoClient kakaoClient; private final MemberService memberService; private final JwtUtil jwtUtil; - @Value(("${FRONT_PAGE_SIGNUP}")) - String redirectPage; - - public ResponseEntity> kakaoLogin(String authorizationCode) { - String kakaoToken = kakaoClient.getAccessToken(authorizationCode); // 인가코드로부터 카카오토큰 발급 - KakaoUserInfo kakaoUserInfo = kakaoClient.getUserInfo((kakaoToken)); - String userKakaoEmail = kakaoUserInfo.kakaoAccount().email(); // 와 사용자 카카오 이메일이야 - - Optional existMember = memberService.findMemberByEmail(userKakaoEmail); - if (existMember.isPresent()) { - String serviceToken = - jwtUtil.generateToken(memberService.getMemberId(userKakaoEmail)); // 서비스 토큰 줘야징 - return ResponseEntity.ok((ApiResponse.with(HttpStatus.OK, "로그인 성공", serviceToken))); - } else { - return ResponseEntity.status(HttpStatus.FOUND) - .location( - URI.create(redirectPage + URLEncoder.encode(userKakaoEmail, StandardCharsets.UTF_8))) - .build(); - } + public AuthService(MemberService memberService, JwtUtil jwtUtil) { + this.memberService = memberService; + this.jwtUtil = jwtUtil; } - public ResponseEntity> register( - String email, String deliveryName, String phoneNumber) { + public String register(String email, String deliveryName, String phoneNumber) { Member member = new Member(email, deliveryName, phoneNumber); memberService.registerMember(member); Long memberId = memberService.getMemberId(email); String serviceToken = jwtUtil.generateToken(memberId); - return ResponseEntity.ok((ApiResponse.with(HttpStatus.OK, "회원가입 및 로그인 성공", serviceToken))); + return serviceToken; + } + + public String getServiceToken(String email) { + return jwtUtil.generateToken(memberService.getMemberId(email)); } } diff --git a/src/main/java/com/ordertogether/team14_be/auth/application/service/KakaoAuthService.java b/src/main/java/com/ordertogether/team14_be/auth/application/service/KakaoAuthService.java new file mode 100644 index 00000000..9c3d2c74 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/auth/application/service/KakaoAuthService.java @@ -0,0 +1,19 @@ +package com.ordertogether.team14_be.auth.application.service; + +import com.ordertogether.team14_be.auth.application.dto.KakaoUserInfo; +import com.ordertogether.team14_be.auth.presentation.KakaoClient; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class KakaoAuthService { + private final KakaoClient kakaoClient; + + public String getKakaoUserEmail(String authorizationCode) { + String kakaoToken = kakaoClient.getAccessToken(authorizationCode); + KakaoUserInfo kakaoUserInfo = kakaoClient.getUserInfo((kakaoToken)); + String userKakaoEmail = kakaoUserInfo.kakaoAccount().email(); + return userKakaoEmail; + } +} diff --git a/src/main/java/com/ordertogether/team14_be/auth/presentation/AuthController.java b/src/main/java/com/ordertogether/team14_be/auth/presentation/AuthController.java index c62ccd68..9ea80103 100644 --- a/src/main/java/com/ordertogether/team14_be/auth/presentation/AuthController.java +++ b/src/main/java/com/ordertogether/team14_be/auth/presentation/AuthController.java @@ -1,9 +1,17 @@ package com.ordertogether.team14_be.auth.presentation; import com.ordertogether.team14_be.auth.application.service.AuthService; +import com.ordertogether.team14_be.auth.application.service.KakaoAuthService; import com.ordertogether.team14_be.common.web.response.ApiResponse; import com.ordertogether.team14_be.member.application.dto.MemberInfoRequest; -import lombok.RequiredArgsConstructor; +import com.ordertogether.team14_be.member.application.service.MemberService; +import com.ordertogether.team14_be.member.persistence.entity.Member; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -13,16 +21,49 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -@RequiredArgsConstructor @RestController @RequestMapping("/api/v1/auth") public class AuthController { private final AuthService authService; + private final KakaoAuthService kakaoAuthService; + private final String redirectPage; + private final MemberService memberService; + + public AuthController( + AuthService authService, + KakaoAuthService kakaoAuthService, + MemberService memberService, + @Value("${FRONT_PAGE_SIGNUP}") String redirectPage) { + this.authService = authService; + this.kakaoAuthService = kakaoAuthService; + this.memberService = memberService; + this.redirectPage = redirectPage; + } @GetMapping("/login") public ResponseEntity> getToken(@RequestHeader String authorizationCode) { - return authService.kakaoLogin(authorizationCode); + String userKakaoEmail = kakaoAuthService.getKakaoUserEmail(authorizationCode); + Optional existMember = memberService.findMemberByEmail(userKakaoEmail); + if (existMember.isPresent()) { + return ResponseEntity.ok( + ApiResponse.with(HttpStatus.OK, "로그인 성공", authService.getServiceToken(userKakaoEmail))); + + } else { + return ResponseEntity.status(HttpStatus.FOUND) + .location( + URI.create(redirectPage + URLEncoder.encode(userKakaoEmail, StandardCharsets.UTF_8))) + .build(); + } + } + + @PostMapping("/signup") + public ResponseEntity> signUpMember( + @RequestParam String email, @RequestBody MemberInfoRequest memberInfoRequest) { + String serviceToken = + authService.register( + email, memberInfoRequest.deliveryName(), memberInfoRequest.phoneNumber()); + return ResponseEntity.ok(ApiResponse.with(HttpStatus.OK, "로그인 성공", serviceToken)); } @PostMapping("/signup") diff --git a/src/main/java/com/ordertogether/team14_be/common/web/response/ApiResponse.java b/src/main/java/com/ordertogether/team14_be/common/web/response/ApiResponse.java index 23ccb295..f0f68931 100644 --- a/src/main/java/com/ordertogether/team14_be/common/web/response/ApiResponse.java +++ b/src/main/java/com/ordertogether/team14_be/common/web/response/ApiResponse.java @@ -10,11 +10,15 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class ApiResponse { - private Integer status; - private String message; - private T data; + private final Integer status; + private final String message; + private final T data; public static ApiResponse with(HttpStatus httpStatus, String message, @Nullable T data) { return new ApiResponse<>(httpStatus.value(), message, data); } + + public static ApiResponse with(HttpStatus httpStatus, String message) { + return new ApiResponse<>(httpStatus.value(), message, null); + } } diff --git a/src/main/java/com/ordertogether/team14_be/config/CorsConfig.java b/src/main/java/com/ordertogether/team14_be/config/CorsConfig.java new file mode 100644 index 00000000..0faa7996 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/config/CorsConfig.java @@ -0,0 +1,19 @@ +package com.ordertogether.team14_be.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry + .addMapping("/**") + .allowedOrigins("*") + .allowedMethods("GET", "POST", "PUT", "DELETE") + .allowedHeaders("Authorization", "Content-Type") + .exposedHeaders("Custom-Header") + .maxAge(3600); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/config/PersistenceConfig.java b/src/main/java/com/ordertogether/team14_be/config/PersistenceConfig.java index 8d24b129..b64acd15 100644 --- a/src/main/java/com/ordertogether/team14_be/config/PersistenceConfig.java +++ b/src/main/java/com/ordertogether/team14_be/config/PersistenceConfig.java @@ -27,8 +27,12 @@ public AuditorAware auditorProvider() { @Bean public PaymentEventRepository paymentEventRepository( - SimpleJpaPaymentEventRepository simpleJpaPaymentEventRepository) { - return new JpaPaymentEventRepository(simpleJpaPaymentEventRepository); + SimpleJpaPaymentEventRepository simpleJpaPaymentEventRepository, + SimpleJpaPaymentOrderRepository simpleJpaPaymentOrderRepository, + SimpleJpaProductRepository simpleJpaProductRepository) { + return new JpaPaymentEventRepository( + simpleJpaPaymentEventRepository, + paymentOrderRepository(simpleJpaPaymentOrderRepository, simpleJpaProductRepository)); } @Bean diff --git a/src/main/java/com/ordertogether/team14_be/member/application/service/MemberService.java b/src/main/java/com/ordertogether/team14_be/member/application/service/MemberService.java index 32f90e74..638cf1d7 100644 --- a/src/main/java/com/ordertogether/team14_be/member/application/service/MemberService.java +++ b/src/main/java/com/ordertogether/team14_be/member/application/service/MemberService.java @@ -1,6 +1,5 @@ package com.ordertogether.team14_be.member.application.service; -import com.ordertogether.team14_be.auth.JwtUtil; import com.ordertogether.team14_be.member.application.dto.MemberInfoResponse; import com.ordertogether.team14_be.member.application.exception.NotFoundMember; import com.ordertogether.team14_be.member.persistence.MemberRepository; @@ -37,6 +36,25 @@ public MemberInfoResponse findMemberInfo(Long memberId) { .build(); } + @Transactional(readOnly = true) + public Long getMemberId(String email) { + return memberRepository + .findByEmail(email) + .map(Member::getId) + .orElseThrow(() -> new NoSuchElementException("Member with email " + email + " not found")); + } + + @Transactional(readOnly = true) + public MemberInfoResponse findMemberInfo(Long memberId) { + Member member = findMember(memberId); + + return MemberInfoResponse.builder() + .deliveryName(member.getDeliveryName()) + .phoneNumber(member.getPhoneNumber()) + .point(member.getPoint()) + .build(); + } + @Transactional public MemberInfoResponse modifyMember(Long memberId, String deliveryName, String phoneNumber) { Member member = findMember(memberId); @@ -65,12 +83,8 @@ public Optional findMemberByEmail(String email) { return memberRepository.findByEmail(email); } + @Transactional public void registerMember(Member member) { memberRepository.saveAndFlush(member); } - - public Long getMemberId(String email) { - Member member = memberRepository.findByEmail(email).get(); - return member.getId(); - } } diff --git a/src/main/java/com/ordertogether/team14_be/member/persistence/entity/Member.java b/src/main/java/com/ordertogether/team14_be/member/persistence/entity/Member.java index 66bcfc8c..b7d03fdc 100644 --- a/src/main/java/com/ordertogether/team14_be/member/persistence/entity/Member.java +++ b/src/main/java/com/ordertogether/team14_be/member/persistence/entity/Member.java @@ -30,6 +30,16 @@ public class Member { protected Member() {} + public Member( + Long id, String email, int point, String phoneNumber, String deliveryName, String platform) { + this.id = id; + this.email = email; + this.point = point; + this.phoneNumber = phoneNumber; + this.deliveryName = deliveryName; + this.platform = platform; + } + public Member(String email, int point, String phoneNumber, String deliveryName, String platform) { this.email = email; this.point = point; @@ -73,18 +83,8 @@ public void modifyMemberInfo(String deliveryName, String phoneNumber) { this.phoneNumber = phoneNumber; } - public void modifyMemberInfo(String deliveryName, String phoneNumber) { - this.deliveryName = deliveryName; - this.phoneNumber = phoneNumber; - } - - public void modifyMemberInfo(String deliveryName, String phoneNumber) { - this.deliveryName = deliveryName; - this.phoneNumber = phoneNumber; - } - - public void modifyMemberInfo(String deliveryName, String phoneNumber) { - this.deliveryName = deliveryName; - this.phoneNumber = phoneNumber; + public Integer increasePoint(int point) { + this.point += point; + return this.point; } } diff --git a/src/main/java/com/ordertogether/team14_be/memebr/application/service/MemberService.java b/src/main/java/com/ordertogether/team14_be/memebr/application/service/MemberService.java new file mode 100644 index 00000000..9ae1f05a --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/memebr/application/service/MemberService.java @@ -0,0 +1,26 @@ +package com.ordertogether.team14_be.memebr.application.service; + +import com.ordertogether.team14_be.memebr.persistence.MemberRepository; +import com.ordertogether.team14_be.memebr.persistence.entity.Member; +import org.springframework.stereotype.Service; + +@Service +public class MemberService { + + private final MemberRepository memberRepository; + + public MemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public void findOrCreateMember(String email) { + Member member = + memberRepository + .findByEmail(email) + .orElseGet( + () -> { + Member newMember = Member.createMember(email); + return memberRepository.saveAndFlush(newMember); + }); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/memebr/persistence/MemberRepository.java b/src/main/java/com/ordertogether/team14_be/memebr/persistence/MemberRepository.java new file mode 100644 index 00000000..cea83bb6 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/memebr/persistence/MemberRepository.java @@ -0,0 +1,12 @@ +package com.ordertogether.team14_be.memebr.persistence; + +import com.ordertogether.team14_be.memebr.persistence.entity.Member; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MemberRepository extends JpaRepository { + + Optional findByEmail(String email); +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentStatus.java b/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentStatus.java index 001acfaa..c8b22524 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentStatus.java +++ b/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentStatus.java @@ -13,4 +13,12 @@ public enum PaymentStatus { FAIL("결제 실패"); private final String description; + + public boolean isSuccess() { + return this == SUCCESS; + } + + public boolean isFail() { + return this == FAIL; + } } diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/entity/PaymentEventEntity.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/entity/PaymentEventEntity.java index 6a64f56d..07fb1010 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/entity/PaymentEventEntity.java +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/entity/PaymentEventEntity.java @@ -40,7 +40,6 @@ public class PaymentEventEntity extends BaseTimeEntity { @Column(nullable = false) private String orderName; - @Column(nullable = false) private String paymentKey; // PSP 결제 식별자 @Builder.Default diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/mapper/PaymentEventMapper.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/mapper/PaymentEventMapper.java index db590005..3dc0af80 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/mapper/PaymentEventMapper.java +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/mapper/PaymentEventMapper.java @@ -1,7 +1,9 @@ package com.ordertogether.team14_be.payment.persistence.jpa.mapper; import com.ordertogether.team14_be.payment.domain.PaymentEvent; +import com.ordertogether.team14_be.payment.domain.PaymentOrder; import com.ordertogether.team14_be.payment.persistence.jpa.entity.PaymentEventEntity; +import java.util.List; import lombok.experimental.UtilityClass; @UtilityClass @@ -18,14 +20,16 @@ public static PaymentEventEntity mapToEntity(PaymentEvent domain) { .build(); } - public static PaymentEvent mapToDomain(PaymentEventEntity entity) { + public static PaymentEvent mapToDomain( + PaymentEventEntity paymentEventEntity, List paymentOrders) { return PaymentEvent.builder() - .id(entity.getId()) - .buyerId(entity.getBuyerId()) - .orderId(entity.getOrderId()) - .orderName(entity.getOrderName()) - .paymentKey(entity.getPaymentKey()) - .paymentStatus(entity.getPaymentStatus()) + .id(paymentEventEntity.getId()) + .buyerId(paymentEventEntity.getBuyerId()) + .paymentOrders(paymentOrders) + .orderId(paymentEventEntity.getOrderId()) + .orderName(paymentEventEntity.getOrderName()) + .paymentKey(paymentEventEntity.getPaymentKey()) + .paymentStatus(paymentEventEntity.getPaymentStatus()) .build(); } } diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentEventRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentEventRepository.java index 7c286b90..5a924265 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentEventRepository.java +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentEventRepository.java @@ -1,33 +1,69 @@ package com.ordertogether.team14_be.payment.persistence.jpa.repository; import com.ordertogether.team14_be.payment.domain.PaymentEvent; +import com.ordertogether.team14_be.payment.domain.PaymentOrder; +import com.ordertogether.team14_be.payment.domain.PaymentStatus; import com.ordertogether.team14_be.payment.persistence.jpa.entity.PaymentEventEntity; import com.ordertogether.team14_be.payment.persistence.jpa.mapper.PaymentEventMapper; import com.ordertogether.team14_be.payment.persistence.repository.PaymentEventRepository; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentOrderRepository; +import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor +@Transactional(readOnly = true) public class JpaPaymentEventRepository implements PaymentEventRepository { private final SimpleJpaPaymentEventRepository simpleJpaPaymentEventRepository; + private final PaymentOrderRepository paymentOrderRepository; @Override + @Transactional public PaymentEvent save(PaymentEvent paymentEvent) { PaymentEventEntity savedEntity = simpleJpaPaymentEventRepository.save(PaymentEventMapper.mapToEntity(paymentEvent)); - return PaymentEventMapper.mapToDomain(savedEntity); - } - - @Override - public Optional findById(Long id) { - return simpleJpaPaymentEventRepository.findById(id).map(PaymentEventMapper::mapToDomain); + return PaymentEventMapper.mapToDomain(savedEntity, paymentEvent.getPaymentOrders()); } @Override public Optional findByOrderId(String orderId) { + List paymentOrders = paymentOrderRepository.findByOrderId(orderId); return simpleJpaPaymentEventRepository .findByOrderId(orderId) - .map(PaymentEventMapper::mapToDomain); + .map(paymentEvent -> PaymentEventMapper.mapToDomain(paymentEvent, paymentOrders)); + } + + @Override + @Transactional + public Integer updatePaymentStatus( + String paymentKey, String orderId, PaymentStatus paymentStatus) { + return simpleJpaPaymentEventRepository.updatePaymentStatus(paymentKey, orderId, paymentStatus); + } + + @Override + @Transactional + public Integer updatePaymentStatusToExecuting(String orderId, String paymentKey) { + checkPreviousPaymentStatus(orderId); + simpleJpaPaymentEventRepository.updatePaymentKey(paymentKey, orderId); + + return simpleJpaPaymentEventRepository.updatePaymentStatus( + paymentKey, orderId, PaymentStatus.EXECUTING); + } + + private void checkPreviousPaymentStatus(String orderId) { + PaymentStatus previousStatus = + findByOrderId(orderId) + .orElseThrow( + () -> + new IllegalArgumentException( + "orderId: %s 에 해당하는 결제 정보가 없습니다.".formatted(orderId))) + .getPaymentStatus(); + + if (previousStatus.isSuccess() || previousStatus.isFail()) { + throw new IllegalArgumentException( + "이미 %s 상태인 결제 입니다.".formatted(previousStatus.getDescription())); + } } } diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentOrderRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentOrderRepository.java index 1eade54e..6db87db5 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentOrderRepository.java +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentOrderRepository.java @@ -6,11 +6,15 @@ import com.ordertogether.team14_be.payment.persistence.jpa.mapper.PaymentOrderMapper; import com.ordertogether.team14_be.payment.persistence.jpa.mapper.ProductMapper; import com.ordertogether.team14_be.payment.persistence.repository.PaymentOrderRepository; +import java.math.BigDecimal; import java.util.List; +import java.util.NoSuchElementException; import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor +@Transactional(readOnly = true) public class JpaPaymentOrderRepository implements PaymentOrderRepository { private final SimpleJpaPaymentOrderRepository simpleJpaPaymentOrderRepository; @@ -24,6 +28,7 @@ public class JpaPaymentOrderRepository implements PaymentOrderRepository { * @return 저장된 결제 주문 정보 */ @Override + @Transactional public PaymentOrder save(PaymentOrder paymentOrder) { addMissingProductInfo(paymentOrder); PaymentOrderEntity savedEntity = @@ -40,6 +45,7 @@ private void addMissingProductInfo(PaymentOrder paymentOrder) { } @Override + @Transactional public List saveAll(List paymentOrders) { List savedEntities = simpleJpaPaymentOrderRepository.saveAll( @@ -53,12 +59,27 @@ public Optional findById(Long id) { return simpleJpaPaymentOrderRepository.findById(id).map(PaymentOrderMapper::mapToDomain); } + @Override + public List findByOrderId(String orderId) { + return simpleJpaPaymentOrderRepository.findByOrderId(orderId).stream() + .map(PaymentOrderMapper::mapToDomain) + .toList(); + } + + @Override + public BigDecimal getPaymentTotalAmount(String orderId) { + return simpleJpaPaymentOrderRepository + .getPaymentTotalAmount(orderId) + .orElseThrow( + () -> new NoSuchElementException("주문 번호: %s 에 해당하는 주문이 존재하지 않습니다.".formatted(orderId))); + } + private ProductEntity getProductEntity(PaymentOrder paymentOrder) { return simpleJpaProductRepository .findById(paymentOrder.getProductId()) .orElseThrow( () -> - new IllegalArgumentException( + new NoSuchElementException( String.format("상품 아이디 %s에 해당하는 상품이 없습니다.", paymentOrder.getProductId()))); } } diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentEventRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentEventRepository.java index dce6ca28..eb870fe2 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentEventRepository.java +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentEventRepository.java @@ -1,12 +1,30 @@ package com.ordertogether.team14_be.payment.persistence.jpa.repository; +import com.ordertogether.team14_be.payment.domain.PaymentStatus; import com.ordertogether.team14_be.payment.persistence.jpa.entity.PaymentEventEntity; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository public interface SimpleJpaPaymentEventRepository extends JpaRepository { Optional findByOrderId(String orderId); + + @Modifying + @Query( + "UPDATE PaymentEventEntity pee" + + " SET pee.paymentStatus = :afterStatus" + + " WHERE pee.paymentKey = :paymentKey" + + " AND pee.orderId = :orderId") + Integer updatePaymentStatus(String paymentKey, String orderId, PaymentStatus afterStatus); + + @Modifying + @Query( + "UPDATE PaymentEventEntity pee" + + " SET pee.paymentKey = :paymentKey" + + " WHERE pee.orderId = :orderId") + Integer updatePaymentKey(String paymentKey, String orderId); } diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentOrderRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentOrderRepository.java index f5c5d38e..5c2c0342 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentOrderRepository.java +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentOrderRepository.java @@ -1,8 +1,18 @@ package com.ordertogether.team14_be.payment.persistence.jpa.repository; import com.ordertogether.team14_be.payment.persistence.jpa.entity.PaymentOrderEntity; +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository -public interface SimpleJpaPaymentOrderRepository extends JpaRepository {} +public interface SimpleJpaPaymentOrderRepository extends JpaRepository { + + @Query("SELECT SUM(po.amount) FROM PaymentOrderEntity po WHERE po.orderId = :orderId") + Optional getPaymentTotalAmount(String orderId); + + List findByOrderId(String orderId); +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentEventRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentEventRepository.java index 7084d746..3872bec6 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentEventRepository.java +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentEventRepository.java @@ -1,13 +1,25 @@ package com.ordertogether.team14_be.payment.persistence.repository; import com.ordertogether.team14_be.payment.domain.PaymentEvent; +import com.ordertogether.team14_be.payment.domain.PaymentStatus; import java.util.Optional; public interface PaymentEventRepository { PaymentEvent save(PaymentEvent paymentEvent); - Optional findById(Long id); - Optional findByOrderId(String orderId); + + Integer updatePaymentStatus(String paymentKey, String orderId, PaymentStatus paymentStatus); + + /** + * 주문 상태를 '실행 중'으로 변경합니다.
+ * - PSP 에서 전달받은 결제 식별자 paymentKey 로 paymentKey 필드를 초기화합니다.
+ * - 주문 상태를 '실행 중'으로 변경합니다. + * + * @param orderId 주문 번호 + * @param paymentKey PSP 에서 전달 받은 결제 식별자 + * @return 변경된 행의 수 + */ + Integer updatePaymentStatusToExecuting(String orderId, String paymentKey); } diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentOrderRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentOrderRepository.java index 7fa05b8b..38eba9c1 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentOrderRepository.java +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentOrderRepository.java @@ -1,6 +1,7 @@ package com.ordertogether.team14_be.payment.persistence.repository; import com.ordertogether.team14_be.payment.domain.PaymentOrder; +import java.math.BigDecimal; import java.util.List; import java.util.Optional; @@ -11,4 +12,14 @@ public interface PaymentOrderRepository { List saveAll(List paymentOrders); Optional findById(Long id); + + List findByOrderId(String orderId); + + /** + * 주문 번호에 해당하는 주문에 대하여 총 결제 금액을 반환한다. + * + * @param orderId 주문번호 + * @return 총 결제 금액 + */ + BigDecimal getPaymentTotalAmount(String orderId); } diff --git a/src/main/java/com/ordertogether/team14_be/payment/service/PaymentConfirmService.java b/src/main/java/com/ordertogether/team14_be/payment/service/PaymentConfirmService.java new file mode 100644 index 00000000..0159a1fa --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/service/PaymentConfirmService.java @@ -0,0 +1,40 @@ +package com.ordertogether.team14_be.payment.service; + +import com.ordertogether.team14_be.payment.service.command.PaymentStatusUpdateCommand; +import com.ordertogether.team14_be.payment.web.request.PaymentConfirmRequest; +import com.ordertogether.team14_be.payment.web.response.PaymentConfirmationResponse; +import com.ordertogether.team14_be.payment.web.toss.client.TossPaymentsClient; +import java.math.BigDecimal; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +/** 결제 승인 서비스 */ +public class PaymentConfirmService { + + private final TossPaymentsClient tossPaymentsClient; + + private final PaymentValidationService paymentValidationService; + private final PaymentStatusUpdateService paymentStatusUpdateService; + private final PointManagementService pointManagementService; + + public PaymentConfirmationResponse confirm(PaymentConfirmRequest request) { + // 1. 결제 상태 변경 (준비 -> 실행 중) + paymentStatusUpdateService.updatePaymentStatusToExecuting( + request.orderId(), request.paymentKey()); + // 2. 결제 유효성 검사 + paymentValidationService.validate(request.orderId(), BigDecimal.valueOf(request.amount())); + // 3. 결제 승인 요청 + PaymentConfirmationResponse response = tossPaymentsClient.confirmPayment(request); + // 4. 승인 결과에 따른 결제 상태 업데이트 + paymentStatusUpdateService.updatePaymentStatus( + new PaymentStatusUpdateCommand( + request.paymentKey(), request.orderId(), response.paymentStatus())); + // 5. 포인트 충전 + pointManagementService.increasePoint(request.orderId()); + return response; + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/service/PaymentPreparationService.java b/src/main/java/com/ordertogether/team14_be/payment/service/PaymentPreparationService.java index 1b2b33cb..a2e18fe8 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/service/PaymentPreparationService.java +++ b/src/main/java/com/ordertogether/team14_be/payment/service/PaymentPreparationService.java @@ -50,15 +50,16 @@ public PaymentPrepareResponse prepare(PaymentPrepareRequest request) { */ private void validateDuplicatePayment(PaymentPrepareRequest request) { String idempotentKey = IdempotentKeyGenerator.generate(request.getIdempotencySeed()); + paymentEventRepository .findByOrderId(idempotentKey) .ifPresent( - paymentEvent -> { + duplicatedPaymentEvent -> { throw new IllegalArgumentException( "Seed: %s 를 통해 생성된 결제는 이미 %s 상태인 주문입니다." .formatted( request.getIdempotencySeed(), - paymentEvent.getPaymentStatus().getDescription())); + duplicatedPaymentEvent.getPaymentStatus().getDescription())); }); } @@ -84,7 +85,6 @@ private PaymentEvent createPaymentEvent(PaymentPrepareRequest request, List createPaymentOrder(product, idempotencySeed)).toList()) .orderId(IdempotentKeyGenerator.generate(idempotencySeed)) .orderName(createOrderName(products)) - .paymentKey(IdempotentKeyGenerator.generate(idempotencySeed)) .build(); } diff --git a/src/main/java/com/ordertogether/team14_be/payment/service/PaymentStatusUpdateService.java b/src/main/java/com/ordertogether/team14_be/payment/service/PaymentStatusUpdateService.java new file mode 100644 index 00000000..5bcb1f94 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/service/PaymentStatusUpdateService.java @@ -0,0 +1,25 @@ +package com.ordertogether.team14_be.payment.service; + +import com.ordertogether.team14_be.payment.persistence.repository.PaymentEventRepository; +import com.ordertogether.team14_be.payment.service.command.PaymentStatusUpdateCommand; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PaymentStatusUpdateService { + + private final PaymentEventRepository paymentEventRepository; + + @Transactional + public Integer updatePaymentStatusToExecuting(String orderId, String paymentKey) { + return paymentEventRepository.updatePaymentStatusToExecuting(orderId, paymentKey); + } + + @Transactional + public Integer updatePaymentStatus(PaymentStatusUpdateCommand command) { + return paymentEventRepository.updatePaymentStatus( + command.paymentKey(), command.orderId(), command.paymentStatus()); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/service/PaymentValidationService.java b/src/main/java/com/ordertogether/team14_be/payment/service/PaymentValidationService.java new file mode 100644 index 00000000..ac7d5fc5 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/service/PaymentValidationService.java @@ -0,0 +1,39 @@ +package com.ordertogether.team14_be.payment.service; + +import com.ordertogether.team14_be.payment.persistence.repository.PaymentOrderRepository; +import java.math.BigDecimal; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +/** 결제 유효성 검사 */ +public class PaymentValidationService { + + private final PaymentOrderRepository paymentOrderRepository; + + /** + * 요청된 결제 금액과 실제 결제 금액이 일치하는 지 검증합니다. + * + * @param orderId 주문 번호 + * @param requestedAmount 요청한 결제 금액 + * @return 결제 금액이 일치하면 true, 그렇지 않으면 {@link IllegalArgumentException} 발생 + */ + public boolean validate(String orderId, BigDecimal requestedAmount) { + BigDecimal expectedAmount = paymentOrderRepository.getPaymentTotalAmount(orderId); + + if (isAmountMismatch(requestedAmount, expectedAmount)) { + throw new IllegalArgumentException( + "주문 번호: %s 의 결제 요청 금액 %s 원은 예상 결제 금액 %s 원과 다릅니다." + .formatted(orderId, requestedAmount, expectedAmount)); + } + + return true; + } + + private boolean isAmountMismatch(BigDecimal requestedAmount, BigDecimal expectedAmount) { + return requestedAmount.compareTo(expectedAmount) != 0; + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/service/PointManagementService.java b/src/main/java/com/ordertogether/team14_be/payment/service/PointManagementService.java new file mode 100644 index 00000000..8a36c47d --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/service/PointManagementService.java @@ -0,0 +1,31 @@ +package com.ordertogether.team14_be.payment.service; + +import com.ordertogether.team14_be.member.application.service.MemberService; +import com.ordertogether.team14_be.payment.domain.PaymentEvent; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentEventRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PointManagementService { + + private final PaymentEventRepository paymentEventRepository; + private final MemberService memberService; + + @Transactional + public Integer increasePoint(String orderId) { + PaymentEvent paymentEvent = + paymentEventRepository + .findByOrderId(orderId) + .orElseThrow( + () -> + new IllegalArgumentException( + "orderId : %s 에 해당하는 PaymentEvent 가 없습니다.".formatted(orderId))); + + return memberService + .findMember(paymentEvent.getBuyerId()) + .increasePoint(paymentEvent.totalAmount().intValue()); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/service/PointUpdateService.java b/src/main/java/com/ordertogether/team14_be/payment/service/PointUpdateService.java new file mode 100644 index 00000000..c3cec251 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/service/PointUpdateService.java @@ -0,0 +1,31 @@ +package com.ordertogether.team14_be.payment.service; + +import com.ordertogether.team14_be.member.application.service.MemberService; +import com.ordertogether.team14_be.payment.domain.PaymentEvent; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentEventRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PointUpdateService { + + private final PaymentEventRepository paymentEventRepository; + private final MemberService memberService; + + @Transactional + public Integer increasePoint(String orderId) { + PaymentEvent paymentEvent = + paymentEventRepository + .findByOrderId(orderId) + .orElseThrow( + () -> + new IllegalArgumentException( + "orderId : %s 에 해당하는 PaymentEvent 가 없습니다.".formatted(orderId))); + + return memberService + .findMember(paymentEvent.getBuyerId()) + .increasePoint(paymentEvent.totalAmount().intValue()); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/service/command/PaymentStatusUpdateCommand.java b/src/main/java/com/ordertogether/team14_be/payment/service/command/PaymentStatusUpdateCommand.java new file mode 100644 index 00000000..54836bd4 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/service/command/PaymentStatusUpdateCommand.java @@ -0,0 +1,23 @@ +package com.ordertogether.team14_be.payment.service.command; + +import com.ordertogether.team14_be.payment.domain.PaymentStatus; +import java.util.Objects; + +public record PaymentStatusUpdateCommand( + String paymentKey, String orderId, PaymentStatus paymentStatus) { + + public PaymentStatusUpdateCommand( + String paymentKey, String orderId, PaymentStatus paymentStatus) { + validateObjectsNonnull(paymentKey, orderId, paymentStatus); + this.paymentKey = paymentKey; + this.orderId = orderId; + this.paymentStatus = paymentStatus; + } + + private void validateObjectsNonnull( + String paymentKey, String orderId, PaymentStatus paymentStatus) { + Objects.requireNonNull(paymentKey, "paymentKey 가 null 인 결제의 상태 변경은 불가능합니다."); + Objects.requireNonNull(orderId, "orderId 가 null 인 결제의 상태 변경은 불가능합니다."); + Objects.requireNonNull(paymentStatus, "paymentStatus 가 null 인 결제 상태 변경은 불가능합니다."); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/controller/PaymentController.java b/src/main/java/com/ordertogether/team14_be/payment/web/controller/PaymentController.java index 5b17246e..e567160d 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/web/controller/PaymentController.java +++ b/src/main/java/com/ordertogether/team14_be/payment/web/controller/PaymentController.java @@ -1,8 +1,11 @@ package com.ordertogether.team14_be.payment.web.controller; import com.ordertogether.team14_be.common.web.response.ApiResponse; +import com.ordertogether.team14_be.payment.service.PaymentConfirmService; import com.ordertogether.team14_be.payment.service.PaymentPreparationService; +import com.ordertogether.team14_be.payment.web.request.PaymentConfirmRequest; import com.ordertogether.team14_be.payment.web.request.PaymentPrepareRequest; +import com.ordertogether.team14_be.payment.web.response.PaymentConfirmationResponse; import com.ordertogether.team14_be.payment.web.response.PaymentPrepareResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -18,6 +21,7 @@ public class PaymentController { private final PaymentPreparationService paymentPreparationService; + private final PaymentConfirmService paymentConfirmService; @PostMapping public ResponseEntity> preparePayment( @@ -28,4 +32,15 @@ public ResponseEntity> preparePayment( return ResponseEntity.ok(ApiResponse.with(HttpStatus.OK, "결제 정보를 저장하였습니다.", data)); } + + @PostMapping("/confirm") + public ResponseEntity> confirmPayment( + @RequestBody PaymentConfirmRequest request) { + PaymentConfirmationResponse data = paymentConfirmService.confirm(request); + if (data.paymentStatus().isFail()) { + return ResponseEntity.badRequest() + .body(ApiResponse.with(HttpStatus.BAD_REQUEST, "결제에 실패하였습니다.", data)); + } + return ResponseEntity.ok(ApiResponse.with(HttpStatus.OK, "결제가 완료되었습니다.", data)); + } } diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/controller/PaymentViewController.java b/src/main/java/com/ordertogether/team14_be/payment/web/controller/PaymentViewController.java new file mode 100644 index 00000000..9cc0572f --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/controller/PaymentViewController.java @@ -0,0 +1,40 @@ +package com.ordertogether.team14_be.payment.web.controller; + +import com.ordertogether.team14_be.payment.service.PaymentPreparationService; +import com.ordertogether.team14_be.payment.web.request.PaymentPrepareRequest; +import com.ordertogether.team14_be.payment.web.response.PaymentPrepareResponse; +import java.util.List; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +@RequiredArgsConstructor +public class PaymentViewController { + + private final PaymentPreparationService paymentPreparationService; + + @GetMapping("/success") + public String successPage() { + return "success"; + } + + @GetMapping("/fail") + public String failPage() { + return "fail"; + } + + @GetMapping("/") + public String preparePayment(Model model) { + PaymentPrepareResponse response = + paymentPreparationService.prepare( + new PaymentPrepareRequest(UUID.randomUUID().toString(), List.of(1L, 2L)) + .addBuyerId(1L)); + + model.addAttribute("orderId", response.orderId()); + model.addAttribute("orderName", response.orderName()); + return "checkout"; + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/request/PaymentConfirmRequest.java b/src/main/java/com/ordertogether/team14_be/payment/web/request/PaymentConfirmRequest.java new file mode 100644 index 00000000..1ca8dda7 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/request/PaymentConfirmRequest.java @@ -0,0 +1,6 @@ +package com.ordertogether.team14_be.payment.web.request; + +import lombok.Builder; + +@Builder +public record PaymentConfirmRequest(String orderId, String paymentKey, Long amount) {} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentConfirmationFailure.java b/src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentConfirmationFailure.java new file mode 100644 index 00000000..6179da5a --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentConfirmationFailure.java @@ -0,0 +1,3 @@ +package com.ordertogether.team14_be.payment.web.response; + +public record PaymentConfirmationFailure(String code, String message) {} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentConfirmationResponse.java b/src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentConfirmationResponse.java new file mode 100644 index 00000000..8d71b363 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentConfirmationResponse.java @@ -0,0 +1,20 @@ +package com.ordertogether.team14_be.payment.web.response; + +import com.ordertogether.team14_be.payment.domain.PaymentStatus; +import jakarta.annotation.Nullable; +import java.util.Objects; + +public record PaymentConfirmationResponse( + PaymentStatus paymentStatus, @Nullable PaymentConfirmationFailure failure) { + + public PaymentConfirmationResponse( + PaymentStatus paymentStatus, PaymentConfirmationFailure failure) { + if (paymentStatus.isFail()) { + Objects.requireNonNull( + failure, "결제 상태가 FAIL 인 경우, PaymentConfirmationFailure 가 null 일 수 없습니다."); + } + + this.paymentStatus = paymentStatus; + this.failure = failure; + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/toss/client/TossPaymentsClient.java b/src/main/java/com/ordertogether/team14_be/payment/web/toss/client/TossPaymentsClient.java new file mode 100644 index 00000000..13a9d2d4 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/toss/client/TossPaymentsClient.java @@ -0,0 +1,53 @@ +package com.ordertogether.team14_be.payment.web.toss.client; + +import com.ordertogether.team14_be.payment.domain.PaymentStatus; +import com.ordertogether.team14_be.payment.web.request.PaymentConfirmRequest; +import com.ordertogether.team14_be.payment.web.response.PaymentConfirmationFailure; +import com.ordertogether.team14_be.payment.web.response.PaymentConfirmationResponse; +import com.ordertogether.team14_be.payment.web.toss.response.TossPaymentsConfirmationResponse; +import java.io.IOException; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.RequestHeadersSpec.ConvertibleClientHttpResponse; + +@Component +@Slf4j +@RequiredArgsConstructor +public class TossPaymentsClient { + + private final RestClient tossRestClient; + private static final String URI = "/v1/payments/confirm"; + private static final String IDEMPOTENCY_HEADER_KEY = "Idempotency-Key"; + + public PaymentConfirmationResponse confirmPayment(PaymentConfirmRequest request) { + return tossRestClient + .post() + .uri(URI) + .header(IDEMPOTENCY_HEADER_KEY, request.paymentKey()) + .body(request) + .exchange( + (req, res) -> { + TossPaymentsConfirmationResponse tossResponse = + res.bodyTo(TossPaymentsConfirmationResponse.class); + + return new PaymentConfirmationResponse( + getPaymentStatus(res), getFailure(tossResponse)); + }); + } + + private static PaymentStatus getPaymentStatus(ConvertibleClientHttpResponse res) + throws IOException { + return res.getStatusCode().isError() ? PaymentStatus.FAIL : PaymentStatus.SUCCESS; + } + + private static PaymentConfirmationFailure getFailure( + TossPaymentsConfirmationResponse tossResponse) { + return Objects.isNull(tossResponse.failure()) + ? null + : new PaymentConfirmationFailure( + tossResponse.failure().code(), tossResponse.failure().message()); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/toss/config/TossPaymentsClientConfig.java b/src/main/java/com/ordertogether/team14_be/payment/web/toss/config/TossPaymentsClientConfig.java new file mode 100644 index 00000000..d3257fde --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/toss/config/TossPaymentsClientConfig.java @@ -0,0 +1,30 @@ +package com.ordertogether.team14_be.payment.web.toss.config; + +import java.util.Base64; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.client.RestClient; + +@ConfigurationProperties(prefix = "pg.toss") +@RequiredArgsConstructor +public class TossPaymentsClientConfig { + + private final String secretKey; + private final String url; + + @Bean + public RestClient tossRestClient(RestClient.Builder builder) { + return builder + .defaultHeader(HttpHeaders.AUTHORIZATION, getBasicAuthorization()) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .baseUrl(url) + .build(); + } + + private String getBasicAuthorization() { + return "Basic " + Base64.getEncoder().encodeToString((secretKey + ":").getBytes()); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/toss/error/TossPaymentsError.java b/src/main/java/com/ordertogether/team14_be/payment/web/toss/error/TossPaymentsError.java new file mode 100644 index 00000000..18f35a19 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/toss/error/TossPaymentsError.java @@ -0,0 +1,59 @@ +package com.ordertogether.team14_be.payment.web.toss.error; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum TossPaymentsError { + ALREADY_PROCESSED_PAYMENT(400, "이미 처리된 결제 입니다."), + PROVIDER_ERROR(400, "일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요."), + EXCEED_MAX_CARD_INSTALLMENT_PLAN(400, "설정 가능한 최대 할부 개월 수를 초과했습니다."), + INVALID_REQUEST(400, "잘못된 요청입니다."), + NOT_ALLOWED_POINT_USE(400, "포인트 사용이 불가한 카드로 카드 포인트 결제에 실패했습니다."), + INVALID_API_KEY(400, "잘못된 시크릿키 연동 정보 입니다."), + INVALID_REJECT_CARD(400, "카드 사용이 거절되었습니다. 카드사 문의가 필요합니다."), + BELOW_MINIMUM_AMOUNT(400, "신용카드는 결제금액이 100원 이상, 계좌는 200원이상부터 결제가 가능합니다."), + INVALID_CARD_EXPIRATION(400, "카드 정보를 다시 확인해주세요. (유효기간)"), + INVALID_STOPPED_CARD(400, "정지된 카드 입니다."), + EXCEED_MAX_DAILY_PAYMENT_COUNT(400, "하루 결제 가능 횟수를 초과했습니다."), + NOT_SUPPORTED_INSTALLMENT_PLAN_CARD_OR_MERCHANT(400, "할부가 지원되지 않는 카드 또는 가맹점 입니다."), + INVALID_CARD_INSTALLMENT_PLAN(400, "할부 개월 정보가 잘못되었습니다."), + NOT_SUPPORTED_MONTHLY_INSTALLMENT_PLAN(400, "할부가 지원되지 않는 카드입니다."), + EXCEED_MAX_PAYMENT_AMOUNT(400, "하루 결제 가능 금액을 초과했습니다."), + NOT_FOUND_TERMINAL_ID(400, "단말기번호(Terminal Id)가 없습니다. 토스페이먼츠로 문의 바랍니다."), + INVALID_AUTHORIZE_AUTH(400, "유효하지 않은 인증 방식입니다."), + INVALID_CARD_LOST_OR_STOLEN(400, "분실 혹은 도난 카드입니다."), + RESTRICTED_TRANSFER_ACCOUNT(400, "계좌는 등록 후 12시간 뒤부터 결제할 수 있습니다. 관련 정책은 해당 은행으로 문의해주세요."), + INVALID_CARD_NUMBER(400, "카드번호를 다시 확인해주세요."), + INVALID_UNREGISTERED_SUBMALL(400, "등록되지 않은 서브몰입니다. 서브몰이 없는 가맹점이라면 안심클릭이나 ISP 결제가 필요합니다."), + NOT_REGISTERED_BUSINESS(400, "등록되지 않은 사업자 번호입니다."), + EXCEED_MAX_ONE_DAY_WITHDRAW_AMOUNT(400, "1일 출금 한도를 초과했습니다."), + EXCEED_MAX_ONE_TIME_WITHDRAW_AMOUNT(400, "1회 출금 한도를 초과했습니다."), + CARD_PROCESSING_ERROR(400, "카드사에서 오류가 발생했습니다."), + EXCEED_MAX_AMOUNT(400, "거래금액 한도를 초과했습니다."), + INVALID_ACCOUNT_INFO_RE_REGISTER(400, "유효하지 않은 계좌입니다. 계좌 재등록 후 시도해주세요."), + NOT_AVAILABLE_PAYMENT(400, "결제가 불가능한 시간대입니다"), + UNAPPROVED_ORDER_ID(400, "아직 승인되지 않은 주문번호입니다."), + UNAUTHORIZED_KEY(401, "인증되지 않은 시크릿 키 혹은 클라이언트 키 입니다."), + REJECT_ACCOUNT_PAYMENT(403, "잔액부족으로 결제에 실패했습니다."), + REJECT_CARD_PAYMENT(403, "한도초과 혹은 잔액부족으로 결제에 실패했습니다."), + REJECT_CARD_COMPANY(403, "결제 승인이 거절되었습니다."), + FORBIDDEN_REQUEST(403, "허용되지 않은 요청입니다."), + REJECT_TOSSPAY_INVALID_ACCOUNT(403, "선택하신 출금 계좌가 출금이체 등록이 되어 있지 않아요. 계좌를 다시 등록해 주세요."), + EXCEED_MAX_AUTH_COUNT(403, "최대 인증 횟수를 초과했습니다. 카드사로 문의해주세요."), + EXCEED_MAX_ONE_DAY_AMOUNT(403, "일일 한도를 초과했습니다."), + NOT_AVAILABLE_BANK(403, "은행 서비스 시간이 아닙니다."), + INVALID_PASSWORD(403, "결제 비밀번호가 일치하지 않습니다."), + INCORRECT_BASIC_AUTH_FORMAT(403, "잘못된 요청입니다. ':' 를 포함해 인코딩해주세요."), + FDS_ERROR( + 403, "[토스페이먼츠] 위험거래가 감지되어 결제가 제한됩니다. 발송된 문자에 포함된 링크를 통해 본인인증 후 결제가 가능합니다. (고객센터: 1644-8051)"), + NOT_FOUND_PAYMENT(404, "존재하지 않는 결제 정보 입니다."), + NOT_FOUND_PAYMENT_SESSION(404, "결제 시간이 만료되어 결제 진행 데이터가 존재하지 않습니다."), + FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING(500, "결제가 완료되지 않았어요. 다시 시도해주세요."), + FAILED_INTERNAL_SYSTEM_PROCESSING(500, "내부 시스템 처리 작업이 실패했습니다. 잠시 후 다시 시도해주세요."), + UNKNOWN_PAYMENT_ERROR(500, "결제에 실패했어요. 같은 문제가 반복된다면 은행이나 카드사로 문의해주세요."), + UNKNOWN(500, "알 수 없는 에러입니다."); + + private final Integer statusCode; + + private final String description; +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/toss/response/TossPaymentsConfirmationResponse.java b/src/main/java/com/ordertogether/team14_be/payment/web/toss/response/TossPaymentsConfirmationResponse.java new file mode 100644 index 00000000..45ef657f --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/toss/response/TossPaymentsConfirmationResponse.java @@ -0,0 +1,104 @@ +package com.ordertogether.team14_be.payment.web.toss.response; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import lombok.Builder; + +@Builder +public record TossPaymentsConfirmationResponse( + String version, + String paymentKey, + String type, + String orderId, + String orderName, + String mId, + String currency, + String method, + BigDecimal totalAmount, + BigDecimal balanceAmount, + String status, + String requestedAt, + String approvedAt, + boolean useEscrow, + String lastTransactionKey, + BigDecimal suppliedAmount, + BigDecimal vat, + boolean cultureExpense, + BigDecimal taxFreeAmount, + int taxExemptionAmount, + List cancels, + boolean expired, + boolean isPartialCancelable, + Card card, + VirtualAccount virtualAccount, + Transfer transfer, + CashReceipt cashReceipt, + List cashReceipts, + Metadata metadata, + String receiptUrl, + EasyPay easyPay, + String country, + Failure failure, + RefundReceiveAccount refundReceiveAccount) { + + public static record Card( + BigDecimal amount, + String issuerCode, + String acquirerCode, + String number, + int installmentPlanMonths, + String approveNo, + boolean useCardPoint, + String cardType, + String ownerType, + String acquireStatus, + boolean isInterestFree, + String interestPayer) {} + + public static record Cancel( + BigDecimal cancelAmount, + String cancelReason, + BigDecimal taxFreeAmount, + int taxExemptionAmount, + BigDecimal refundableAmount, + BigDecimal easyPayDiscountAmount, + String canceledAt, + String transactionKey, + String receiptKey, + String cancelStatus, + String cancelRequestId) {} + + public static record CashReceipt( + String type, + String receiptKey, + String issueNumber, + String receiptUrl, + BigDecimal amount, + BigDecimal taxFreeAmount, + String businessNumber, + String transactionType, + String issueStatus, + String customerIdentityNumber, + String requestedAt) {} + + public static record EasyPay(String provider, BigDecimal amount, BigDecimal discountAmount) {} + + public static record Failure(String code, String message) {} + + public static record Metadata(Map metadata) {} + + public static record RefundReceiveAccount( + String bankCode, String accountNumber, String holderName) {} + + public static record Transfer(String bankCode, String settlementStatus) {} + + public static record VirtualAccount( + String accountType, + String accountNumber, + String bankCode, + String customerName, + String dueDate, + String refundStatus, + boolean expired) {} +} diff --git a/src/main/java/com/ordertogether/team14_be/spot/controller/SpotController.java b/src/main/java/com/ordertogether/team14_be/spot/controller/SpotController.java index a7cc2cc6..4e047ed8 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/controller/SpotController.java +++ b/src/main/java/com/ordertogether/team14_be/spot/controller/SpotController.java @@ -39,10 +39,10 @@ public ResponseEntity getSpotDetail(@PathVariable Long id) { } // 반경 n미터 내 Spot 조회하기 - @GetMapping("/api/v1/spot/{lat}/{lng}/{radius}") // 현재 위치의 좌표와 반지름을 받아옴 - public ResponseEntity> getSpotByRadius( - @PathVariable BigDecimal lat, @PathVariable BigDecimal lng, @PathVariable int radius) { - return ResponseEntity.ok(spotService.getSpotByRadius(lat, lng, radius)); + @GetMapping("/api/v1/spot/{lat}/{lng}") // 현재 위치의 좌표로 hash값이 같은 튜플을 조회 + public ResponseEntity> getSpotByGeoHash( + @PathVariable BigDecimal lat, @PathVariable BigDecimal lng) { + return ResponseEntity.ok(spotService.getSpotByGeoHash(lat, lng)); } // Spot 수정하기 diff --git a/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationRequest.java b/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationRequest.java index d6e62e45..baadc5a3 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationRequest.java +++ b/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationRequest.java @@ -3,6 +3,7 @@ import com.ordertogether.team14_be.spot.enums.Category; import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; +import java.time.LocalTime; public record SpotCreationRequest( Long id, @@ -12,4 +13,5 @@ public record SpotCreationRequest( @NotNull(message = "카테고리를 선택해주세요") Category category, @NotNull(message = "최소 주문 금액을 입력해주세요") Integer minimumOrderAmount, @NotNull(message = "배달의 민족 함께 주문링크를 입력해주세요") String togetherOrderLink, - @NotNull(message = "픽업 장소를 입력해주세요") String pickUpLocation) {} + @NotNull(message = "픽업 장소를 입력해주세요") String pickUpLocation, + @NotNull(message = "주문 마감 시간을 입력해주세요") LocalTime deadlineTime) {} diff --git a/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationResponse.java b/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationResponse.java index 8695c545..bb065525 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationResponse.java +++ b/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationResponse.java @@ -1,10 +1,12 @@ package com.ordertogether.team14_be.spot.dto.controllerdto; import com.ordertogether.team14_be.spot.enums.Category; +import java.time.LocalTime; public record SpotCreationResponse( Long id, Category category, String storeName, Integer minimumOrderAmount, - String pickUpLocation) {} + String pickUpLocation, + LocalTime deadlineTime) {} diff --git a/src/main/java/com/ordertogether/team14_be/spot/dto/servicedto/SpotDto.java b/src/main/java/com/ordertogether/team14_be/spot/dto/servicedto/SpotDto.java index d33bba55..c71c67ee 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/dto/servicedto/SpotDto.java +++ b/src/main/java/com/ordertogether/team14_be/spot/dto/servicedto/SpotDto.java @@ -4,6 +4,7 @@ import jakarta.persistence.Column; import java.math.BigDecimal; import java.time.LocalDateTime; +import java.time.LocalTime; import lombok.*; @Builder @@ -25,6 +26,8 @@ public class SpotDto { private String togetherOrderLink; private String pickUpLocation; private String deliveryStatus; + private LocalTime deadlineTime; + @Setter private String geoHash; private boolean isDeleted; private LocalDateTime createdAt; private LocalDateTime modifiedAt; diff --git a/src/main/java/com/ordertogether/team14_be/spot/entity/Spot.java b/src/main/java/com/ordertogether/team14_be/spot/entity/Spot.java index 20373b01..6d2f9906 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/entity/Spot.java +++ b/src/main/java/com/ordertogether/team14_be/spot/entity/Spot.java @@ -5,6 +5,7 @@ import com.ordertogether.team14_be.spot.enums.Category; import jakarta.persistence.*; import java.math.BigDecimal; +import java.time.LocalTime; import lombok.*; import lombok.experimental.SuperBuilder; import org.hibernate.annotations.DynamicUpdate; @@ -42,6 +43,8 @@ public class Spot extends BaseEntity { private String pickUpLocation; private String deliveryStatus; + private LocalTime deadlineTime; + private String geoHash; @Builder.Default private Boolean isDeleted = false; public void delete() { diff --git a/src/main/java/com/ordertogether/team14_be/spot/exception/ErrorResponse.java b/src/main/java/com/ordertogether/team14_be/spot/exception/ErrorResponse.java index 40bb3bef..24effc29 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/exception/ErrorResponse.java +++ b/src/main/java/com/ordertogether/team14_be/spot/exception/ErrorResponse.java @@ -2,5 +2,5 @@ import com.ordertogether.team14_be.spot.enums.ErrorCode; -// @Getter + public record ErrorResponse(String message, ErrorCode code) {} diff --git a/src/main/java/com/ordertogether/team14_be/spot/repository/SimpleSpotRepository.java b/src/main/java/com/ordertogether/team14_be/spot/repository/SimpleSpotRepository.java index 416031a2..f326be19 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/repository/SimpleSpotRepository.java +++ b/src/main/java/com/ordertogether/team14_be/spot/repository/SimpleSpotRepository.java @@ -22,4 +22,6 @@ List findAroundSpotAndIsDeletedFalse( @Param("maxlng") BigDecimal maxlng, @Param("minlat") BigDecimal minlat, @Param("minlng") BigDecimal minlng); + + List findByGeoHash(String geoHash); } diff --git a/src/main/java/com/ordertogether/team14_be/spot/repository/SpotRepository.java b/src/main/java/com/ordertogether/team14_be/spot/repository/SpotRepository.java index 4eed28c5..7e3217ee 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/repository/SpotRepository.java +++ b/src/main/java/com/ordertogether/team14_be/spot/repository/SpotRepository.java @@ -41,10 +41,9 @@ public void delete(Long id) { spot.delete(); } - public List findAroundSpotAndIsDeletedFalse( - BigDecimal maxX, BigDecimal maxY, BigDecimal minX, BigDecimal minY) { - List spots = simpleSpotRepository.findAroundSpotAndIsDeletedFalse(maxX, maxY, minX, minY); - - return spots.stream().map(SpotMapper.INSTANCE::toDto).toList(); + public List findBygeoHash(String geoHash) { + return simpleSpotRepository.findByGeoHash(geoHash).stream() + .map(SpotMapper.INSTANCE::toDto) + .toList(); } } diff --git a/src/main/java/com/ordertogether/team14_be/spot/service/SpotService.java b/src/main/java/com/ordertogether/team14_be/spot/service/SpotService.java index 0ec524d5..509ac61d 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/service/SpotService.java +++ b/src/main/java/com/ordertogether/team14_be/spot/service/SpotService.java @@ -1,7 +1,6 @@ package com.ordertogether.team14_be.spot.service; -import static java.lang.Math.abs; - +import ch.hsr.geohash.GeoHash; import com.ordertogether.team14_be.spot.dto.controllerdto.SpotCreationResponse; import com.ordertogether.team14_be.spot.dto.controllerdto.SpotDetailResponse; import com.ordertogether.team14_be.spot.dto.controllerdto.SpotViewedResponse; @@ -18,7 +17,6 @@ @Service @RequiredArgsConstructor public class SpotService { - public static final int EARTH_RADIUS = 6371000; // 6371km private final SpotRepository spotRepository; // Spot 전체 조회하기 @@ -31,6 +29,10 @@ public List getSpot(BigDecimal lat, BigDecimal lng) { @Transactional public SpotCreationResponse createSpot(SpotDto spotDto) { + BigDecimal lat = spotDto.getLat(); + BigDecimal lng = spotDto.getLng(); + GeoHash geoHash = GeoHash.withCharacterPrecision(lat.doubleValue(), lng.doubleValue(), 12); + spotDto.setGeoHash(geoHash.toBase32()); Spot spot = SpotMapper.INSTANCE.toEntity(spotDto, new Spot()); return SpotMapper.INSTANCE.toSpotCreationResponse(spotRepository.save(spot)); } @@ -42,42 +44,16 @@ public SpotDetailResponse getSpot(Long id) { return SpotMapper.INSTANCE.toSpotDetailResponse(spotDto); } - // 반경 n미터 내 Spot 조회하기 @Transactional(readOnly = true) - public List getSpotByRadius(BigDecimal lat, BigDecimal lng, int radius) { - // m당 y 좌표 이동 값 - double mForLatitude = (1 / (EARTH_RADIUS * 1 * (Math.PI / 180))) / 1000; - // m당 x 좌표 이동 값 - double mForLongitude = - (1 / (EARTH_RADIUS * 1 * (Math.PI / 180) * Math.cos(Math.toRadians(lat.doubleValue())))) - / 1000; - - // 현재 위치 기준 검색 거리 좌표 - double maxY = lat.doubleValue() + (radius * mForLatitude); - double minY = lat.doubleValue() - (radius * mForLatitude); - double maxX = lng.doubleValue() + (radius * mForLongitude); - double minX = lng.doubleValue() - (radius * mForLongitude); + public List getSpotByGeoHash(BigDecimal lat, BigDecimal lng) { + int precision = 12; + GeoHash geoHash = + GeoHash.withCharacterPrecision(lat.doubleValue(), lng.doubleValue(), precision); - // 원의 지름에 해당하는 정사각형 내에 있는 Spot들을 모두 가져옴 - List resultAroundSpot = - spotRepository.findAroundSpotAndIsDeletedFalse( - BigDecimal.valueOf(maxX), - BigDecimal.valueOf(maxY), - BigDecimal.valueOf(minX), - BigDecimal.valueOf(minY)); + String hashString = geoHash.toBase32(); - // 자기 위치에서부터 반경 내에 있는 Spot만 반환 - return resultAroundSpot.stream() - .filter( - spotDto -> { - double distance = - Math.sqrt( - Math.pow(abs(spotDto.getLat().doubleValue() - lat.doubleValue()), 2) - + Math.pow(abs(spotDto.getLng().doubleValue() - lng.doubleValue()), 2)); - return distance <= radius; - }) - .map(SpotMapper.INSTANCE::toSpotViewedResponse) - .toList(); + List resultAroundSpot = spotRepository.findBygeoHash(hashString); + return resultAroundSpot.stream().map(SpotMapper.INSTANCE::toSpotViewedResponse).toList(); } @Transactional diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 95a30cc9..249b0064 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -40,6 +40,11 @@ kakao: api: url: ${KAKAO_USER_API_URL} +pg: + toss: + secret-key: test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6 + url: https://api.tosspayments.com + key: jwt: secret-key: ${JWT_SECRET_KEY} diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 59b6346f..b3610722 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,3 +1,5 @@ +INSERT INTO member (id, email, point, phone_number, delivery_name, platform) VALUES (1, 'member1@example.com', 100000, '010-1234-5678', 'John Doe', 'Kakao'); + INSERT INTO product (id, name, price, created_at, modified_at, created_by, modified_by) VALUES (1, 'Product 1', 10000, now(), now(), 1, 1); INSERT INTO product (id, name, price, created_at, modified_at, created_by, modified_by) VALUES (2, 'Product 2', 20000, now(), now(), 1, 1); INSERT INTO product (id, name, price, created_at, modified_at, created_by, modified_by) VALUES (3, 'Product 3', 30000, now(), now(), 1, 1); diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css new file mode 100644 index 00000000..f6dd93b1 --- /dev/null +++ b/src/main/resources/static/style.css @@ -0,0 +1,251 @@ +body { + background-image: url('https://static.toss.im/ml-illust/img-back_005.jpg'); +} +.p { + padding: 0; + margin: 0; + font-family: Toss Product Sans, -apple-system, BlinkMacSystemFont, + Bazier Square, Noto Sans KR, Segoe UI, Apple SD Gothic Neo, Roboto, + Helvetica Neue, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, + Segoe UI Symbol, Noto Color Emoji; + color: #4e5968; + word-break: keep-all; + word-wrap: break-word; +} +.h4 { + font-size: 20px; + font-weight: 700; + color: #333D4B; +} +.wrapper { + max-width: 800px; + margin: 0 auto; +} +.button { + color: #f9fafb; + background-color: #3182f6; + margin: 0; + font-size: 15px; + font-weight: 400; + line-height: 18px; + white-space: nowrap; + text-align: center; + /* vertical-align: middle; */ + cursor: pointer; + border: 0 solid transparent; + user-select: none; + transition: background 0.2s ease, color 0.1s ease; + text-decoration: none; + border-radius: 7px; + padding: 11px 16px; +} +.button:hover { + color: #fff; + background-color: #1b64da; +} +.title { + margin: 0 0 4px; + font-size: 24px; + font-weight: 600; + color: #4e5968; +} +.result { + flex-direction: column; + align-items: center; + text-align: center; + text-wrap: balance; +} +.box_section { + background-color: white; + border-radius: 10px; + box-shadow: 0 10px 20px rgb(0 0 0 / 1%), 0 6px 6px rgb(0 0 0 / 6%); + padding: 40px 30px 50px 30px; + margin-top:30px; + margin-bottom:50px; + color: #333D4B +} +:root { + --checkable-size: 20px; + --checkable-input-top: 3px; + --checkable-input-left: 5px; + --checkable-input-width: 14px; + --checkable-input-height: 10px; + --checkable-input-svg: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.343 4.574l4.243 4.243 7.07-7.071' fill='transparent' stroke-width='2' stroke='%23FFF' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + --checkable-label-text-padding: 8px; + --indeterminate-checkable-input-top: 7px; + --indeterminate-checkable-input-left: 5px; + --indeterminate-checkable-input-width: 14px +} + +:root .checkable--small { + --checkable-size: 20px; + --checkable-input-top: 2px; + --checkable-input-left: 4px; + --checkable-input-width: 12px; + --checkable-input-height: 9px; + --checkable-input-svg: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.286 3.645l3.536 3.536 5.892-5.893' fill='transparent' stroke-width='2' stroke='%23FFF' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + --indeterminate-checkable-input-top: 5px; + --indeterminate-checkable-input-left: 4px; + --indeterminate-checkable-input-width: 12px +} + +.checkable { + position: relative; + display: flex +} + +.checkable+.checkable { + margin-top: 12px +} + +.checkable--inline { + display: inline-block +} + +.checkable--inline+.checkable--inline { + margin-top: 0; + margin-left: 18px +} + +.checkable__label { + display: inline-block; + max-width: 100%; + min-height: 20px; + min-height: var(--checkable-size); + line-height: 1.6; + padding-left: 20px; + padding-left: var(--checkable-size); + margin-bottom: 0; + padding-top: 0; + padding-bottom: 0; + color: #4e5968; + color: var(--grey700); + cursor: pointer +} + +.checkable__input { + position: absolute; + margin: 0 0 0 -20px; + margin: 0 0 0 calc(var(--checkable-size)*-1); + top: 4px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: none; + cursor: pointer +} + +.checkable__input:after,.checkable__input:before { + content: ""; + position: absolute +} + +.checkable__input:before { + top: -4px; + left: 0; + width: 20px; + width: var(--checkable-size); + height: 20px; + height: var(--checkable-size); + border: 2px solid #d1d6db; + border: 2px solid #d1d6db; + background-color: #fff; + background-color: white; + transition: border-color .1s ease,background-color .1s ease +} + +.checkable__input:after { + opacity: 0; + transition: opacity .1s ease; + top: 3px; + top: var(--checkable-input-top); + left: 5px; + left: var(--checkable-input-left); + width: 14px; + width: var(--checkable-input-width); + height: 10px; + height: var(--checkable-input-height); + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.343 4.574l4.243 4.243 7.07-7.071' fill='transparent' stroke-width='2' stroke='%23FFF' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-image: var(--checkable-input-svg); + background-repeat: no-repeat +} + +.checkable__input[type=checkbox]:indeterminate:after { + top: 7px; + top: var(--indeterminate-checkable-input-top); + left: 5px; + left: var(--indeterminate-checkable-input-left); + width: 14px; + width: var(--indeterminate-checkable-input-width); + height: 0; + border: 1px solid #fff; + border: 1px solid var(--white); + border-radius: 1px; + transform: rotate(0) +} + +.checkable__input:focus { + outline: 0 +} + +.checkable__input:focus:before,.checkable__input:hover:before { + background-color: #e8f3ff; + background-color: #e8f3ff; + border-color: #3182f6; + border-color: #3182f6 +} + +.checkable__input:checked:before,.checkable__input[type=checkbox]:indeterminate:before { + border-color: #3182f6; + border-color: #3182f6; + background-color: #3182f6; + background-color: #3182f6 +} + +.checkable__input:checked:after,.checkable__input[type=checkbox]:indeterminate:after { + opacity: 1 +} + +.checkable__input:disabled:before { + background-color: #f2f4f6; + background-color: var(--grey100); + border-color: rgba(0,23,51,.02); + border-color: var(--greyOpacity50) +} + +.checkable__input:disabled:checked:before,.checkable__input:disabled[type=checkbox]:indeterminate:before { + background-color: #e5e8eb; + background-color: var(--grey200); + border-color: #e5e8eb; + border-color: var(--grey200) +} + +.checkable__input[type=checkbox]:before { + border-radius: 6px +} + +.checkable__input[type=radio]:before { + border-radius: 12px +} + +.checkable__label-text { + display: inline-block; + padding-left: 13px; + color: #4e5968; + + /* padding-left: var(--checkable-label-text-padding) */ +} + +.checkable--disabled>.checkable__input { + cursor: not-allowed +} + +.checkable--disabled>.checkable__label { + color: #b0b8c1; + color: var(--grey400); + cursor: not-allowed +} + +.checkable--read-only { + pointer-events: none +} diff --git a/src/main/resources/templates/checkout.html b/src/main/resources/templates/checkout.html new file mode 100644 index 00000000..98f50b90 --- /dev/null +++ b/src/main/resources/templates/checkout.html @@ -0,0 +1,147 @@ + + + + + + + + + 토스페이먼츠 샘플 프로젝트 + + + + + + +
+
+ +
+ +
+ +
+
+ +
+
+ + +
+
+ + + + +
+
+ + + diff --git a/src/main/resources/templates/fail.html b/src/main/resources/templates/fail.html new file mode 100644 index 00000000..0f298edd --- /dev/null +++ b/src/main/resources/templates/fail.html @@ -0,0 +1,34 @@ + + + + + + + + + 토스페이먼츠 샘플 프로젝트 + + + +
+
+

+ + 결제 실패 +

+

+

+
+
+ + + + diff --git a/src/main/resources/templates/success.html b/src/main/resources/templates/success.html new file mode 100644 index 00000000..603d01af --- /dev/null +++ b/src/main/resources/templates/success.html @@ -0,0 +1,67 @@ + + + + + + + + + 토스페이먼츠 샘플 프로젝트 + + +
+
+

+ + 결제 성공 +

+ +

+

+

+
+
+ + + diff --git a/src/test/java/com/ordertogether/team14_be/helper/PaymentDatabaseHelper.java b/src/test/java/com/ordertogether/team14_be/helper/PaymentDatabaseHelper.java index 73e969bd..c8b44021 100644 --- a/src/test/java/com/ordertogether/team14_be/helper/PaymentDatabaseHelper.java +++ b/src/test/java/com/ordertogether/team14_be/helper/PaymentDatabaseHelper.java @@ -3,4 +3,8 @@ public interface PaymentDatabaseHelper { void clean(); + + void saveTestData(); + + void setOrderId(String orderId); } diff --git a/src/test/java/com/ordertogether/team14_be/helper/jpa/JpaPaymentDatabaseHelper.java b/src/test/java/com/ordertogether/team14_be/helper/jpa/JpaPaymentDatabaseHelper.java index 7f292b01..c2b6da2e 100644 --- a/src/test/java/com/ordertogether/team14_be/helper/jpa/JpaPaymentDatabaseHelper.java +++ b/src/test/java/com/ordertogether/team14_be/helper/jpa/JpaPaymentDatabaseHelper.java @@ -1,17 +1,92 @@ package com.ordertogether.team14_be.helper.jpa; import com.ordertogether.team14_be.helper.PaymentDatabaseHelper; +import com.ordertogether.team14_be.member.persistence.MemberRepository; +import com.ordertogether.team14_be.member.persistence.entity.Member; +import com.ordertogether.team14_be.payment.domain.PaymentEvent; +import com.ordertogether.team14_be.payment.domain.PaymentOrder; +import com.ordertogether.team14_be.payment.domain.PaymentStatus; +import com.ordertogether.team14_be.payment.domain.Product; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentEventRepository; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentOrderRepository; +import com.ordertogether.team14_be.payment.persistence.repository.ProductRepository; +import java.math.BigDecimal; +import java.util.List; +import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; @Component @RequiredArgsConstructor public class JpaPaymentDatabaseHelper implements PaymentDatabaseHelper { private final JpaDatabaseCleanup jpaDatabaseCleanup; + private final PaymentEventRepository paymentEventRepository; + private final PaymentOrderRepository paymentOrderRepository; + private final ProductRepository productRepository; + private final MemberRepository memberRepository; + + private String orderId; @Override public void clean() { jpaDatabaseCleanup.execute(); } + + @Override + @Transactional + public void saveTestData() { + if (Objects.isNull(orderId)) { + throw new IllegalStateException("orderId is not set"); + } + memberRepository.save( + new Member(1L, "member1@example.com", 100000, "010-1234-5678", "member1", "Kakao")); + + productRepository.saveAll( + List.of( + Product.builder().id(1L).name("Product 1").price(BigDecimal.valueOf(10000)).build(), + Product.builder().id(2L).name("Product 2").price(BigDecimal.valueOf(20000)).build(), + Product.builder().id(3L).name("Product 3").price(BigDecimal.valueOf(30000)).build())); + + List paymentOrders = + paymentOrderRepository.saveAll( + List.of( + PaymentOrder.builder() + .id(1L) + .productId(1L) + .orderId(orderId) + .orderName("Product 1") + .amount(BigDecimal.valueOf(10000L)) + .build(), + PaymentOrder.builder() + .id(2L) + .productId(2L) + .orderId(orderId) + .orderName("Product 2") + .amount(BigDecimal.valueOf(20000L)) + .build(), + PaymentOrder.builder() + .id(3L) + .productId(3L) + .orderId(orderId) + .orderName("Product 3") + .amount(BigDecimal.valueOf(30000L)) + .build())); + + paymentEventRepository.save( + PaymentEvent.builder() + .id(1L) + .buyerId(1L) + .orderId(orderId) + .paymentOrders(paymentOrders) + .orderName("Product 1, Product 2, Product 3") + .paymentStatus(PaymentStatus.READY) + .build()); + } + + @Override + public void setOrderId(String orderId) { + this.orderId = orderId; + } } diff --git a/src/test/java/com/ordertogether/team14_be/payment/service/PaymentConfirmServiceTest.java b/src/test/java/com/ordertogether/team14_be/payment/service/PaymentConfirmServiceTest.java new file mode 100644 index 00000000..b5543afd --- /dev/null +++ b/src/test/java/com/ordertogether/team14_be/payment/service/PaymentConfirmServiceTest.java @@ -0,0 +1,101 @@ +package com.ordertogether.team14_be.payment.service; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.ordertogether.team14_be.helper.PaymentDatabaseHelper; +import com.ordertogether.team14_be.member.persistence.MemberRepository; +import com.ordertogether.team14_be.payment.domain.PaymentStatus; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentEventRepository; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentOrderRepository; +import com.ordertogether.team14_be.payment.persistence.repository.ProductRepository; +import com.ordertogether.team14_be.payment.web.request.PaymentConfirmRequest; +import com.ordertogether.team14_be.payment.web.response.PaymentConfirmationResponse; +import com.ordertogether.team14_be.payment.web.toss.client.TossPaymentsClient; +import java.util.NoSuchElementException; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ExtendWith(MockitoExtension.class) +@ActiveProfiles(profiles = "test") +class PaymentConfirmServiceTest { + + @MockBean PaymentConfirmService paymentConfirmService; + + @Autowired PaymentDatabaseHelper paymentDatabaseHelper; + + @Autowired PaymentValidationService paymentValidationService; + @Autowired PaymentStatusUpdateService paymentStatusUpdateService; + @Autowired PointManagementService pointManagementService; + + @Autowired PaymentOrderRepository paymentOrderRepository; + @Autowired PaymentEventRepository paymentEventRepository; + @Autowired ProductRepository productRepository; + @Autowired MemberRepository memberRepository; + + @Mock TossPaymentsClient tossPaymentsClient; + + @BeforeEach + void setUp() { + paymentConfirmService = + new PaymentConfirmService( + tossPaymentsClient, + paymentValidationService, + paymentStatusUpdateService, + pointManagementService); + + paymentDatabaseHelper.clean(); + + paymentDatabaseHelper.setOrderId("test-order-id"); + paymentDatabaseHelper.saveTestData(); + } + + @Test + @DisplayName("결제 승인 성공 시, 결제 상태를 성공으로 저장하고 포인트를 증가시킨다") + void shouldSaveSuccessStatusWhenNormallyRequest() { + // given + int beforePoint = + memberRepository + .findById(1L) + .orElseThrow(() -> new NoSuchElementException("Member not found")) + .getPoint(); + + long chargeAmount = 60000L; + PaymentConfirmRequest request = + PaymentConfirmRequest.builder() + .orderId("test-order-id") + .paymentKey(UUID.randomUUID().toString()) + .amount(chargeAmount) + .build(); + + // when + when(tossPaymentsClient.confirmPayment(any(PaymentConfirmRequest.class))) + .thenReturn(new PaymentConfirmationResponse(PaymentStatus.SUCCESS, null)); + + PaymentConfirmationResponse response = paymentConfirmService.confirm(request); + + // then + int afterPoint = + memberRepository + .findById(1L) + .orElseThrow(() -> new NoSuchElementException("Member not found")) + .getPoint(); + + assertAll( + () -> assertThat(afterPoint).isEqualTo(beforePoint + chargeAmount), + () -> assertThat(response.paymentStatus()).isEqualTo(PaymentStatus.SUCCESS), + () -> assertThat(response.failure()).isNull()); + } +} diff --git a/src/test/java/com/ordertogether/team14_be/payment/service/PaymentPreparationServiceTest.java b/src/test/java/com/ordertogether/team14_be/payment/service/PaymentPreparationServiceTest.java index 30ac7813..dff20c96 100644 --- a/src/test/java/com/ordertogether/team14_be/payment/service/PaymentPreparationServiceTest.java +++ b/src/test/java/com/ordertogether/team14_be/payment/service/PaymentPreparationServiceTest.java @@ -54,7 +54,7 @@ void shouldSuccessWhenNormalRequest() { assertThat(response.paymentOrders()).hasSize(3); assertThat(response.orderId()).isNotNull(); assertThat(response.orderName()).isEqualTo("Product 1,Product 2,Product 3"); - assertThat(response.paymentKey()).isNotNull(); + assertThat(response.paymentKey()).isNull(); response.paymentOrders().stream() .forEach( paymentOrder -> { diff --git a/src/test/java/com/ordertogether/team14_be/payment/service/PaymentStatusUpdateServiceTest.java b/src/test/java/com/ordertogether/team14_be/payment/service/PaymentStatusUpdateServiceTest.java new file mode 100644 index 00000000..297fd49f --- /dev/null +++ b/src/test/java/com/ordertogether/team14_be/payment/service/PaymentStatusUpdateServiceTest.java @@ -0,0 +1,72 @@ +package com.ordertogether.team14_be.payment.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ordertogether.team14_be.helper.PaymentDatabaseHelper; +import com.ordertogether.team14_be.payment.domain.PaymentEvent; +import com.ordertogether.team14_be.payment.domain.PaymentOrder; +import com.ordertogether.team14_be.payment.domain.PaymentStatus; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentEventRepository; +import com.ordertogether.team14_be.payment.service.command.PaymentStatusUpdateCommand; +import java.math.BigDecimal; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +class PaymentStatusUpdateServiceTest { + + @Autowired PaymentStatusUpdateService paymentStatusUpdateService; + + @Autowired PaymentDatabaseHelper paymentDatabaseHelper; + + @Autowired PaymentEventRepository paymentEventRepository; + + @BeforeEach + void setup() { + paymentDatabaseHelper.clean(); + + paymentEventRepository.save( + PaymentEvent.builder() + .id(1L) + .paymentOrders( + List.of( + PaymentOrder.builder() + .id(1L) + .orderId("test-order-id") + .orderName("test-order-name01") + .amount(BigDecimal.valueOf(1000)) + .productId(1L) + .build(), + PaymentOrder.builder() + .id(2L) + .orderId("test-order-id") + .orderName("test-order-name02") + .amount(BigDecimal.valueOf(2000)) + .productId(2L) + .build())) + .paymentStatus(PaymentStatus.READY) + .buyerId(1L) + .paymentKey("test-payment-key") + .orderId("test-order-id") + .orderName("test-order-name01, test-order-name02") + .build()); + } + + @ParameterizedTest(name = "PaymentEvent의 상태를 READY 에서 {0} 으로 변경할 수 있다.") + @EnumSource + void shouldUpdateStatusWithNormalRequest(PaymentStatus paymentStatus) { + PaymentStatusUpdateCommand command = + new PaymentStatusUpdateCommand("test-payment-key", "test-order-id", paymentStatus); + paymentStatusUpdateService.updatePaymentStatus(command); + + PaymentEvent result = paymentEventRepository.findByOrderId("test-order-id").get(); + + assertThat(result.getPaymentStatus()).isEqualTo(paymentStatus); + } +} diff --git a/src/test/java/com/ordertogether/team14_be/payment/service/PointManagementServiceTest.java b/src/test/java/com/ordertogether/team14_be/payment/service/PointManagementServiceTest.java new file mode 100644 index 00000000..9daadf15 --- /dev/null +++ b/src/test/java/com/ordertogether/team14_be/payment/service/PointManagementServiceTest.java @@ -0,0 +1,61 @@ +package com.ordertogether.team14_be.payment.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ordertogether.team14_be.helper.PaymentDatabaseHelper; +import com.ordertogether.team14_be.member.persistence.MemberRepository; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentEventRepository; +import java.util.NoSuchElementException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles(profiles = "test") +class PointManagementServiceTest { + + @Autowired PointManagementService pointManagementService; + + @Autowired MemberRepository memberRepository; + @Autowired PaymentEventRepository paymentEventRepository; + + @Autowired PaymentDatabaseHelper paymentDatabaseHelper; + + @BeforeEach + void setUp() { + paymentDatabaseHelper.clean(); + + paymentDatabaseHelper.setOrderId("test-order-id"); + paymentDatabaseHelper.saveTestData(); + } + + @Test + @DisplayName("구매 금액만큼 포인트가 증가한다") + void shouldIncreaseSuccessWhenNormallyRequest() { + // given + int beforePoint = + memberRepository + .findById(1L) + .orElseThrow(() -> new NoSuchElementException("Member not found")) + .getPoint(); + Long chargeAmount = + paymentEventRepository + .findByOrderId("test-order-id") + .orElseThrow(() -> new NoSuchElementException("PaymentEvent not found")) + .totalAmount(); + + // when + pointManagementService.increasePoint("test-order-id"); + + // then + int afterPoint = + memberRepository + .findById(1L) + .orElseThrow(() -> new NoSuchElementException("Member not found")) + .getPoint(); + assertThat(afterPoint).isEqualTo(beforePoint + chargeAmount); + } +} diff --git a/src/test/java/com/ordertogether/team14_be/payment/service/PointUpdateServiceTest.java b/src/test/java/com/ordertogether/team14_be/payment/service/PointUpdateServiceTest.java new file mode 100644 index 00000000..89abc11f --- /dev/null +++ b/src/test/java/com/ordertogether/team14_be/payment/service/PointUpdateServiceTest.java @@ -0,0 +1,62 @@ +package com.ordertogether.team14_be.payment.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import com.ordertogether.team14_be.helper.PaymentDatabaseHelper; +import com.ordertogether.team14_be.member.persistence.MemberRepository; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentEventRepository; +import java.util.NoSuchElementException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles(profiles = "test") +class PointUpdateServiceTest { + + @Autowired PointUpdateService pointUpdateService; + + @Autowired MemberRepository memberRepository; + @Autowired PaymentEventRepository paymentEventRepository; + + @Autowired PaymentDatabaseHelper paymentDatabaseHelper; + + @BeforeEach + void setUp() { + paymentDatabaseHelper.clean(); + + paymentDatabaseHelper.setOrderId("test-order-id"); + paymentDatabaseHelper.saveTestData(); + } + + @Test + @DisplayName("구매 금액만큼 포인트가 증가한다") + void shouldIncreaseSuccessWhenNormallyRequest() { + // given + int beforePoint = + memberRepository + .findById(1L) + .orElseThrow(() -> new NoSuchElementException("Member not found")) + .getPoint(); + Long chargeAmount = + paymentEventRepository + .findByOrderId("test-order-id") + .orElseThrow(() -> new NoSuchElementException("PaymentEvent not found")) + .totalAmount(); + + // when + pointUpdateService.increasePoint("test-order-id"); + + // then + int afterPoint = + memberRepository + .findById(1L) + .orElseThrow(() -> new NoSuchElementException("Member not found")) + .getPoint(); + assertThat(afterPoint).isEqualTo(beforePoint + chargeAmount); + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index d780e3f5..edb4685d 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -42,6 +42,18 @@ kakao: api: url: ${KAKAO_USER_API_URL} +pg: + toss: + secret-key: test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6 + url: https://api.tosspayments.com + key: jwt: secret-key: ${JWT_SECRET_KEY} + +jwt: + expire-time: 1 + +front: + page: + signup: ${FRONT_PAGE_SIGNUP} From 53179a6f2930086910c26326191739a3537e2020 Mon Sep 17 00:00:00 2001 From: BOMIN LYU <83059096+rbm0524@users.noreply.github.com> Date: Sat, 9 Nov 2024 11:27:00 +0900 Subject: [PATCH 2/3] =?UTF-8?q?10=EC=A3=BC=EC=B0=A8=20Develop=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20(#94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Squash merge: Develop branch changes * fix: 사용하지 않는 클래스들 삭제 --- .../auth/presentation/AuthController.java | 80 +++++++--- .../application/service/MemberService.java | 26 ---- .../memebr/persistence/MemberRepository.java | 12 -- .../payment/domain/PaymentStatus.java | 9 ++ .../repository/JpaPaymentOrderRepository.java | 7 + .../SimpleJpaPaymentOrderRepository.java | 10 ++ .../repository/PaymentOrderRepository.java | 4 + .../service/PaymentHistoryService.java | 22 +++ .../web/controller/PaymentController.java | 24 ++- .../web/controller/PointController.java | 26 ++++ .../payment/web/dto/PaymentHistory.java | 6 + .../web/request/PaymentHistoryRequest.java | 3 + .../payment/web/request/UsePointRequest.java | 4 + .../web/response/PaymentHistoryResponse.java | 8 + .../payment/web/response/PointResponse.java | 6 + .../spot/controller/SpotController.java | 7 - .../converter/AbstractCodedEnumConverter.java | 11 +- .../spot/converter/CategoryConverter.java | 12 ++ .../controllerdto/SpotCreationRequest.java | 3 +- .../controllerdto/SpotCreationResponse.java | 5 +- .../dto/controllerdto/SpotDetailResponse.java | 4 +- .../dto/controllerdto/SpotViewedResponse.java | 4 +- .../spot/dto/servicedto/SpotDto.java | 10 +- .../team14_be/spot/entity/Spot.java | 3 + .../team14_be/spot/enums/Category.java | 52 +++++-- .../team14_be/spot/enums/ErrorCode.java | 11 +- .../spot/exception/ErrorResponse.java | 6 - .../exception/NotSpotMasterException.java | 15 ++ .../spot/exception/SpotExceptionHandler.java | 20 ++- .../team14_be/spot/mapper/SpotMapper.java | 21 ++- .../spot/repository/SimpleSpotRepository.java | 10 -- .../team14_be/spot/service/SpotService.java | 22 ++- src/main/resources/application.yml | 1 + src/main/resources/data.sql | 23 ++- .../helper/PaymentDatabaseHelper.java | 2 - .../helper/jpa/JpaPaymentDatabaseHelper.java | 141 +++++++++++++++--- .../service/PaymentConfirmServiceTest.java | 4 +- .../service/PaymentValidationServiceTest.java | 4 + .../service/PointManagementServiceTest.java | 6 +- .../spot/unit/SpotRepositoryTest.java | 130 ++++++++++++++++ .../team14_be/spot/unit/SpotServiceTest.java | 131 ++++++++++++++++ 41 files changed, 742 insertions(+), 163 deletions(-) delete mode 100644 src/main/java/com/ordertogether/team14_be/memebr/application/service/MemberService.java delete mode 100644 src/main/java/com/ordertogether/team14_be/memebr/persistence/MemberRepository.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/service/PaymentHistoryService.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/web/controller/PointController.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/web/dto/PaymentHistory.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/web/request/PaymentHistoryRequest.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/web/request/UsePointRequest.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentHistoryResponse.java create mode 100644 src/main/java/com/ordertogether/team14_be/payment/web/response/PointResponse.java create mode 100644 src/main/java/com/ordertogether/team14_be/spot/converter/CategoryConverter.java delete mode 100644 src/main/java/com/ordertogether/team14_be/spot/exception/ErrorResponse.java create mode 100644 src/main/java/com/ordertogether/team14_be/spot/exception/NotSpotMasterException.java create mode 100644 src/test/java/com/ordertogether/team14_be/spot/unit/SpotRepositoryTest.java create mode 100644 src/test/java/com/ordertogether/team14_be/spot/unit/SpotServiceTest.java diff --git a/src/main/java/com/ordertogether/team14_be/auth/presentation/AuthController.java b/src/main/java/com/ordertogether/team14_be/auth/presentation/AuthController.java index 9ea80103..c1342214 100644 --- a/src/main/java/com/ordertogether/team14_be/auth/presentation/AuthController.java +++ b/src/main/java/com/ordertogether/team14_be/auth/presentation/AuthController.java @@ -6,22 +6,23 @@ import com.ordertogether.team14_be.member.application.dto.MemberInfoRequest; import com.ordertogether.team14_be.member.application.service.MemberService; import com.ordertogether.team14_be.member.persistence.entity.Member; -import java.net.URI; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; import java.util.Optional; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -@RestController +@Controller @RequestMapping("/api/v1/auth") public class AuthController { @@ -42,18 +43,39 @@ public AuthController( } @GetMapping("/login") - public ResponseEntity> getToken(@RequestHeader String authorizationCode) { + public ResponseEntity> getToken( + @RequestHeader("Authorization") String authorizationHeader, + HttpServletResponse httpServletResponse) { + String authorizationCode = authorizationHeader.replace("Bearer ", ""); + System.out.println("인가코드:" + authorizationCode); String userKakaoEmail = kakaoAuthService.getKakaoUserEmail(authorizationCode); + System.out.println("이메일:" + userKakaoEmail); Optional existMember = memberService.findMemberByEmail(userKakaoEmail); if (existMember.isPresent()) { - return ResponseEntity.ok( - ApiResponse.with(HttpStatus.OK, "로그인 성공", authService.getServiceToken(userKakaoEmail))); + String serviceToken = authService.getServiceToken(userKakaoEmail); + ResponseCookie cookie = + ResponseCookie.from("serviceToken", serviceToken) + .httpOnly(true) + .secure(true) + .path("/") + .sameSite("Strict") + .build(); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.SET_COOKIE, cookie.toString()); + + return ResponseEntity.ok() + .headers(headers) + .body(ApiResponse.with(HttpStatus.OK, "로그인 성공", serviceToken)); } else { - return ResponseEntity.status(HttpStatus.FOUND) - .location( - URI.create(redirectPage + URLEncoder.encode(userKakaoEmail, StandardCharsets.UTF_8))) - .build(); + String redirectUrl = redirectPage + userKakaoEmail; + try { + httpServletResponse.sendRedirect(redirectUrl); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + return ResponseEntity.ok().body(ApiResponse.with(HttpStatus.OK, "리다이렉트", redirectUrl)); } } @@ -63,13 +85,35 @@ public ResponseEntity> signUpMember( String serviceToken = authService.register( email, memberInfoRequest.deliveryName(), memberInfoRequest.phoneNumber()); - return ResponseEntity.ok(ApiResponse.with(HttpStatus.OK, "로그인 성공", serviceToken)); + + ResponseCookie cookie = + ResponseCookie.from("serviceToken", serviceToken) + .httpOnly(true) + .secure(true) + .path("/") + .sameSite("Strict") + .build(); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.SET_COOKIE, cookie.toString()); + + return ResponseEntity.ok() + .headers(headers) + .body(ApiResponse.with(HttpStatus.OK, "회원가입 성공", serviceToken)); } - @PostMapping("/signup") - public ResponseEntity> signUpMember( - @RequestParam String email, @RequestBody MemberInfoRequest memberInfoRequest) { - return authService.register( - email, memberInfoRequest.deliveryName(), memberInfoRequest.phoneNumber()); + @PostMapping("/logout") + public void logout(HttpServletResponse response) { + ResponseCookie deleteCookie = + ResponseCookie.from("serviceToken", "") + .maxAge(0) + .httpOnly(true) + .secure(true) + .path("/") + .sameSite("Strict") + .build(); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.SET_COOKIE, deleteCookie.toString()); } } diff --git a/src/main/java/com/ordertogether/team14_be/memebr/application/service/MemberService.java b/src/main/java/com/ordertogether/team14_be/memebr/application/service/MemberService.java deleted file mode 100644 index 9ae1f05a..00000000 --- a/src/main/java/com/ordertogether/team14_be/memebr/application/service/MemberService.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.ordertogether.team14_be.memebr.application.service; - -import com.ordertogether.team14_be.memebr.persistence.MemberRepository; -import com.ordertogether.team14_be.memebr.persistence.entity.Member; -import org.springframework.stereotype.Service; - -@Service -public class MemberService { - - private final MemberRepository memberRepository; - - public MemberService(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } - - public void findOrCreateMember(String email) { - Member member = - memberRepository - .findByEmail(email) - .orElseGet( - () -> { - Member newMember = Member.createMember(email); - return memberRepository.saveAndFlush(newMember); - }); - } -} diff --git a/src/main/java/com/ordertogether/team14_be/memebr/persistence/MemberRepository.java b/src/main/java/com/ordertogether/team14_be/memebr/persistence/MemberRepository.java deleted file mode 100644 index cea83bb6..00000000 --- a/src/main/java/com/ordertogether/team14_be/memebr/persistence/MemberRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.ordertogether.team14_be.memebr.persistence; - -import com.ordertogether.team14_be.memebr.persistence.entity.Member; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface MemberRepository extends JpaRepository { - - Optional findByEmail(String email); -} diff --git a/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentStatus.java b/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentStatus.java index c8b22524..79f45496 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentStatus.java +++ b/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentStatus.java @@ -1,5 +1,6 @@ package com.ordertogether.team14_be.payment.domain; +import java.util.Arrays; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -21,4 +22,12 @@ public boolean isSuccess() { public boolean isFail() { return this == FAIL; } + + public static PaymentStatus fromString(String statusName) { + return Arrays.stream(PaymentStatus.values()) + .filter(paymentStatus -> paymentStatus.name().equalsIgnoreCase(statusName)) + .findFirst() + .orElseThrow( + () -> new IllegalArgumentException("%s 는 올바른 결제 상태가 아닙니다.".formatted(statusName))); + } } diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentOrderRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentOrderRepository.java index 6db87db5..d8b9e7f1 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentOrderRepository.java +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentOrderRepository.java @@ -1,11 +1,13 @@ package com.ordertogether.team14_be.payment.persistence.jpa.repository; import com.ordertogether.team14_be.payment.domain.PaymentOrder; +import com.ordertogether.team14_be.payment.domain.PaymentStatus; import com.ordertogether.team14_be.payment.persistence.jpa.entity.PaymentOrderEntity; import com.ordertogether.team14_be.payment.persistence.jpa.entity.ProductEntity; import com.ordertogether.team14_be.payment.persistence.jpa.mapper.PaymentOrderMapper; import com.ordertogether.team14_be.payment.persistence.jpa.mapper.ProductMapper; import com.ordertogether.team14_be.payment.persistence.repository.PaymentOrderRepository; +import com.ordertogether.team14_be.payment.web.dto.PaymentHistory; import java.math.BigDecimal; import java.util.List; import java.util.NoSuchElementException; @@ -74,6 +76,11 @@ public BigDecimal getPaymentTotalAmount(String orderId) { () -> new NoSuchElementException("주문 번호: %s 에 해당하는 주문이 존재하지 않습니다.".formatted(orderId))); } + @Override + public List getChargeHistory(Long memberId, PaymentStatus paymentStatus) { + return simpleJpaPaymentOrderRepository.getChargeHistory(memberId, paymentStatus); + } + private ProductEntity getProductEntity(PaymentOrder paymentOrder) { return simpleJpaProductRepository .findById(paymentOrder.getProductId()) diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentOrderRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentOrderRepository.java index 5c2c0342..221edf0f 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentOrderRepository.java +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentOrderRepository.java @@ -1,6 +1,8 @@ package com.ordertogether.team14_be.payment.persistence.jpa.repository; +import com.ordertogether.team14_be.payment.domain.PaymentStatus; import com.ordertogether.team14_be.payment.persistence.jpa.entity.PaymentOrderEntity; +import com.ordertogether.team14_be.payment.web.dto.PaymentHistory; import java.math.BigDecimal; import java.util.List; import java.util.Optional; @@ -15,4 +17,12 @@ public interface SimpleJpaPaymentOrderRepository extends JpaRepository getPaymentTotalAmount(String orderId); List findByOrderId(String orderId); + + @Query( + "SELECT new com.ordertogether.team14_be.payment.web.dto.PaymentHistory(poe.amount, poe.createdAt) FROM PaymentOrderEntity poe" + + " WHERE poe.orderId IN " + + " (SELECT bpee.orderId " + + " FROM PaymentEventEntity bpee " + + " WHERE bpee.buyerId = :memberId AND bpee.paymentStatus = :paymentStatus) ") + List getChargeHistory(Long memberId, PaymentStatus paymentStatus); } diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentOrderRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentOrderRepository.java index 38eba9c1..eb016512 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentOrderRepository.java +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentOrderRepository.java @@ -1,6 +1,8 @@ package com.ordertogether.team14_be.payment.persistence.repository; import com.ordertogether.team14_be.payment.domain.PaymentOrder; +import com.ordertogether.team14_be.payment.domain.PaymentStatus; +import com.ordertogether.team14_be.payment.web.dto.PaymentHistory; import java.math.BigDecimal; import java.util.List; import java.util.Optional; @@ -22,4 +24,6 @@ public interface PaymentOrderRepository { * @return 총 결제 금액 */ BigDecimal getPaymentTotalAmount(String orderId); + + List getChargeHistory(Long memberId, PaymentStatus paymentStatus); } diff --git a/src/main/java/com/ordertogether/team14_be/payment/service/PaymentHistoryService.java b/src/main/java/com/ordertogether/team14_be/payment/service/PaymentHistoryService.java new file mode 100644 index 00000000..547426f9 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/service/PaymentHistoryService.java @@ -0,0 +1,22 @@ +package com.ordertogether.team14_be.payment.service; + +import com.ordertogether.team14_be.payment.domain.PaymentStatus; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentOrderRepository; +import com.ordertogether.team14_be.payment.web.dto.PaymentHistory; +import com.ordertogether.team14_be.payment.web.response.PaymentHistoryResponse; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PaymentHistoryService { + + private final PaymentOrderRepository paymentOrderRepository; + + public PaymentHistoryResponse getChargeHistory(Long memberId, PaymentStatus paymentStatus) { + List histories = + paymentOrderRepository.getChargeHistory(memberId, paymentStatus); + return PaymentHistoryResponse.builder().histories(histories).build(); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/controller/PaymentController.java b/src/main/java/com/ordertogether/team14_be/payment/web/controller/PaymentController.java index e567160d..80935875 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/web/controller/PaymentController.java +++ b/src/main/java/com/ordertogether/team14_be/payment/web/controller/PaymentController.java @@ -1,15 +1,22 @@ package com.ordertogether.team14_be.payment.web.controller; import com.ordertogether.team14_be.common.web.response.ApiResponse; +import com.ordertogether.team14_be.member.persistence.entity.Member; +import com.ordertogether.team14_be.member.presentation.LoginMember; +import com.ordertogether.team14_be.payment.domain.PaymentStatus; import com.ordertogether.team14_be.payment.service.PaymentConfirmService; +import com.ordertogether.team14_be.payment.service.PaymentHistoryService; import com.ordertogether.team14_be.payment.service.PaymentPreparationService; import com.ordertogether.team14_be.payment.web.request.PaymentConfirmRequest; +import com.ordertogether.team14_be.payment.web.request.PaymentHistoryRequest; import com.ordertogether.team14_be.payment.web.request.PaymentPrepareRequest; import com.ordertogether.team14_be.payment.web.response.PaymentConfirmationResponse; +import com.ordertogether.team14_be.payment.web.response.PaymentHistoryResponse; import com.ordertogether.team14_be.payment.web.response.PaymentPrepareResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -22,12 +29,12 @@ public class PaymentController { private final PaymentPreparationService paymentPreparationService; private final PaymentConfirmService paymentConfirmService; + private final PaymentHistoryService paymentHistoryService; @PostMapping public ResponseEntity> preparePayment( - @RequestBody PaymentPrepareRequest request) { - // todo: 1L -> UserDetail.getUserId() - request.addBuyerId(1L); + @RequestBody PaymentPrepareRequest request, @LoginMember Member member) { + request.addBuyerId(member.getId()); PaymentPrepareResponse data = paymentPreparationService.prepare(request); return ResponseEntity.ok(ApiResponse.with(HttpStatus.OK, "결제 정보를 저장하였습니다.", data)); @@ -43,4 +50,15 @@ public ResponseEntity> confirmPayment( } return ResponseEntity.ok(ApiResponse.with(HttpStatus.OK, "결제가 완료되었습니다.", data)); } + + @GetMapping("/history") + public ResponseEntity> getHistory( + @RequestBody PaymentHistoryRequest request, @LoginMember Member member) { + return ResponseEntity.ok( + ApiResponse.with( + HttpStatus.OK, + "포인트 사용 내역을 조회하였습니다.", + paymentHistoryService.getChargeHistory( + member.getId(), PaymentStatus.fromString(request.paymentStatus())))); + } } diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/controller/PointController.java b/src/main/java/com/ordertogether/team14_be/payment/web/controller/PointController.java new file mode 100644 index 00000000..3b6bf491 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/controller/PointController.java @@ -0,0 +1,26 @@ +package com.ordertogether.team14_be.payment.web.controller; + +import com.ordertogether.team14_be.common.web.response.ApiResponse; +import com.ordertogether.team14_be.payment.web.request.UsePointRequest; +import com.ordertogether.team14_be.payment.web.response.PointResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/points") +public class PointController { + + @PutMapping + public ResponseEntity> usePoint(@RequestBody UsePointRequest request) { + return ResponseEntity.ok( + ApiResponse.with(HttpStatus.OK, "포인트 사용이 완료되었습니다.", createUsePointResponse())); + } + + private PointResponse createUsePointResponse() { + return PointResponse.builder().remainingPoint(100000).build(); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/dto/PaymentHistory.java b/src/main/java/com/ordertogether/team14_be/payment/web/dto/PaymentHistory.java new file mode 100644 index 00000000..5651d872 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/dto/PaymentHistory.java @@ -0,0 +1,6 @@ +package com.ordertogether.team14_be.payment.web.dto; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +public record PaymentHistory(BigDecimal amount, LocalDateTime date) {} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/request/PaymentHistoryRequest.java b/src/main/java/com/ordertogether/team14_be/payment/web/request/PaymentHistoryRequest.java new file mode 100644 index 00000000..19406f2d --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/request/PaymentHistoryRequest.java @@ -0,0 +1,3 @@ +package com.ordertogether.team14_be.payment.web.request; + +public record PaymentHistoryRequest(String paymentStatus) {} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/request/UsePointRequest.java b/src/main/java/com/ordertogether/team14_be/payment/web/request/UsePointRequest.java new file mode 100644 index 00000000..ce9114f9 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/request/UsePointRequest.java @@ -0,0 +1,4 @@ +package com.ordertogether.team14_be.payment.web.request; + +public record UsePointRequest(Integer paymentPoint // 사용할 포인트 총액 + ) {} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentHistoryResponse.java b/src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentHistoryResponse.java new file mode 100644 index 00000000..27f002f5 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentHistoryResponse.java @@ -0,0 +1,8 @@ +package com.ordertogether.team14_be.payment.web.response; + +import com.ordertogether.team14_be.payment.web.dto.PaymentHistory; +import java.util.List; +import lombok.Builder; + +@Builder +public record PaymentHistoryResponse(List histories) {} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/response/PointResponse.java b/src/main/java/com/ordertogether/team14_be/payment/web/response/PointResponse.java new file mode 100644 index 00000000..35d49a28 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/response/PointResponse.java @@ -0,0 +1,6 @@ +package com.ordertogether.team14_be.payment.web.response; + +import lombok.Builder; + +@Builder +public record PointResponse(Integer remainingPoint) {} diff --git a/src/main/java/com/ordertogether/team14_be/spot/controller/SpotController.java b/src/main/java/com/ordertogether/team14_be/spot/controller/SpotController.java index 4e047ed8..d54f7578 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/controller/SpotController.java +++ b/src/main/java/com/ordertogether/team14_be/spot/controller/SpotController.java @@ -16,13 +16,6 @@ public class SpotController { private final SpotService spotService; - // Spot 전체 조회하기 - @GetMapping("/api/v1/spot/{lat}/{lng}") - public ResponseEntity> getSpot( - @PathVariable BigDecimal lat, @PathVariable BigDecimal lng) { - return ResponseEntity.ok(spotService.getSpot(lat, lng)); - } - // Spot 생성하기 @PostMapping("/api/v1/spot") public ResponseEntity createSpot( diff --git a/src/main/java/com/ordertogether/team14_be/spot/converter/AbstractCodedEnumConverter.java b/src/main/java/com/ordertogether/team14_be/spot/converter/AbstractCodedEnumConverter.java index 3117cbab..6b028a7d 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/converter/AbstractCodedEnumConverter.java +++ b/src/main/java/com/ordertogether/team14_be/spot/converter/AbstractCodedEnumConverter.java @@ -1,29 +1,28 @@ package com.ordertogether.team14_be.spot.converter; import jakarta.persistence.AttributeConverter; -import jakarta.persistence.Converter; import java.util.Arrays; import java.util.Objects; -@Converter -public class AbstractCodedEnumConverter & CodedEnum, E> +public abstract class AbstractCodedEnumConverter & CodedEnum, E> implements AttributeConverter { private final Class clazz; - public AbstractCodedEnumConverter(Class clazz) { + protected AbstractCodedEnumConverter(Class clazz) { this.clazz = clazz; } @SuppressWarnings("unchecked") // 경고 억제 @Override + // Entity의 enum값을 DB에 변환하는 방식을 정의 public E convertToDatabaseColumn( T attribute) { // Converts the value stored in the entity attribute into the data // representation to be stored in the database. - // return attribute.getCode(); -> 코드 저장 ex) KOREAN_STEW -> "찜, 탕, 찌개" - return (E) attribute.name(); // Enum의 이름을 그대로 반환 -> DB에 ENUM을 그대로 저장 + return attribute.getCode(); // 코드 저장 ex) KOREAN_STEW -> "002" } + // DB값을 Entity의 enum값으로 변환하는 방식을 정의 @Override public T convertToEntityAttribute( E dbData) { // Converts the data stored in the database column into the value to be stored diff --git a/src/main/java/com/ordertogether/team14_be/spot/converter/CategoryConverter.java b/src/main/java/com/ordertogether/team14_be/spot/converter/CategoryConverter.java new file mode 100644 index 00000000..eff04250 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/spot/converter/CategoryConverter.java @@ -0,0 +1,12 @@ +package com.ordertogether.team14_be.spot.converter; + +import com.ordertogether.team14_be.spot.enums.Category; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class CategoryConverter extends AbstractCodedEnumConverter { + + public CategoryConverter() { + super(Category.class); // Category.class를 부모 클래스에 전달 + } +} diff --git a/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationRequest.java b/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationRequest.java index baadc5a3..d1e28d3b 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationRequest.java +++ b/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationRequest.java @@ -1,6 +1,5 @@ package com.ordertogether.team14_be.spot.dto.controllerdto; -import com.ordertogether.team14_be.spot.enums.Category; import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.time.LocalTime; @@ -10,7 +9,7 @@ public record SpotCreationRequest( BigDecimal lat, BigDecimal lng, @NotNull(message = "가게 이름을 입력해주세요") String storeName, - @NotNull(message = "카테고리를 선택해주세요") Category category, + @NotNull(message = "카테고리를 선택해주세요") String category, @NotNull(message = "최소 주문 금액을 입력해주세요") Integer minimumOrderAmount, @NotNull(message = "배달의 민족 함께 주문링크를 입력해주세요") String togetherOrderLink, @NotNull(message = "픽업 장소를 입력해주세요") String pickUpLocation, diff --git a/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationResponse.java b/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationResponse.java index bb065525..2b62bbee 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationResponse.java +++ b/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotCreationResponse.java @@ -1,11 +1,12 @@ package com.ordertogether.team14_be.spot.dto.controllerdto; -import com.ordertogether.team14_be.spot.enums.Category; import java.time.LocalTime; +import lombok.Builder; +@Builder public record SpotCreationResponse( Long id, - Category category, + String category, String storeName, Integer minimumOrderAmount, String pickUpLocation, diff --git a/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotDetailResponse.java b/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotDetailResponse.java index e0dce369..19fb7f17 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotDetailResponse.java +++ b/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotDetailResponse.java @@ -1,9 +1,7 @@ package com.ordertogether.team14_be.spot.dto.controllerdto; -import com.ordertogether.team14_be.spot.enums.Category; - public record SpotDetailResponse( - Category category, + String category, String storeName, Integer minimumOrderAmount, String pickUpLocation, diff --git a/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotViewedResponse.java b/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotViewedResponse.java index f51ac0cb..a3677740 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotViewedResponse.java +++ b/src/main/java/com/ordertogether/team14_be/spot/dto/controllerdto/SpotViewedResponse.java @@ -1,6 +1,4 @@ package com.ordertogether.team14_be.spot.dto.controllerdto; -import com.ordertogether.team14_be.spot.enums.Category; - public record SpotViewedResponse( - Category category, String storeName, Integer minimumOrderAmount, String pickUpLocation) {} + String category, String storeName, Integer minimumOrderAmount, String pickUpLocation) {} diff --git a/src/main/java/com/ordertogether/team14_be/spot/dto/servicedto/SpotDto.java b/src/main/java/com/ordertogether/team14_be/spot/dto/servicedto/SpotDto.java index c71c67ee..de6260ee 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/dto/servicedto/SpotDto.java +++ b/src/main/java/com/ordertogether/team14_be/spot/dto/servicedto/SpotDto.java @@ -20,7 +20,7 @@ public class SpotDto { @Column(precision = 11, scale = 8) private BigDecimal lng; - private Category category; + @Setter private Category category; private String storeName; private int minimumOrderAmount; private String togetherOrderLink; @@ -29,8 +29,8 @@ public class SpotDto { private LocalTime deadlineTime; @Setter private String geoHash; private boolean isDeleted; - private LocalDateTime createdAt; - private LocalDateTime modifiedAt; - private Long createdBy; - private Long modifiedBy; + @Setter private LocalDateTime createdAt; + @Setter private LocalDateTime modifiedAt; + @Setter private Long createdBy; + @Setter private Long modifiedBy; } diff --git a/src/main/java/com/ordertogether/team14_be/spot/entity/Spot.java b/src/main/java/com/ordertogether/team14_be/spot/entity/Spot.java index 6d2f9906..d42a942a 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/entity/Spot.java +++ b/src/main/java/com/ordertogether/team14_be/spot/entity/Spot.java @@ -2,6 +2,7 @@ import com.ordertogether.team14_be.common.persistence.entity.BaseEntity; import com.ordertogether.team14_be.member.persistence.entity.Member; +import com.ordertogether.team14_be.spot.converter.CategoryConverter; import com.ordertogether.team14_be.spot.enums.Category; import jakarta.persistence.*; import java.math.BigDecimal; @@ -33,7 +34,9 @@ public class Spot extends BaseEntity { @Column(precision = 11, scale = 8) private BigDecimal lng; + @Convert(converter = CategoryConverter.class) private Category category; + private String storeName; private Integer minimumOrderAmount; diff --git a/src/main/java/com/ordertogether/team14_be/spot/enums/Category.java b/src/main/java/com/ordertogether/team14_be/spot/enums/Category.java index 5c26d0bc..58f70e9d 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/enums/Category.java +++ b/src/main/java/com/ordertogether/team14_be/spot/enums/Category.java @@ -2,29 +2,45 @@ import com.ordertogether.team14_be.spot.converter.AbstractCodedEnumConverter; import com.ordertogether.team14_be.spot.converter.CodedEnum; +import java.util.Arrays; +import java.util.Optional; import lombok.AllArgsConstructor; import lombok.Getter; @AllArgsConstructor @Getter public enum Category implements CodedEnum { - JOKBAL_BOSSAM("족발, 보쌈"), - JAPANESE_FOOD("돈까스, 회, 일식"), - MEAT("고기"), - KOREAN_STEW("찜, 탕, 찌개"), - WESTERN_STYLE("양식"), - CHINESE_FOOD("중식"), - ASIAN("아시안"), - CHICKEN("치킨"), - CARBOHYDRATE("백반, 죽, 국수"), - BURGER("버거"), - K_SNACK_FOOD("분식"), - CAFE("카페, 디저트"); + JOKBAL_BOSSAM("001", "족발, 보쌈"), + KOREAN_STEW("002", "찜, 탕, 찌개"), + JAPANESE_FOOD("003", "돈까스, 회, 일식"), + PIZZA("004", "피자"), + MEAT("005", "고기, 구이"), + NIGHT_FOOD("006", "야식"), + WESTERN_STYLE("007", "양식"), + CHICKEN("008", "치킨"), + CHINESE_FOOD("009", "중식"), + ASIAN("010", "아시안"), + CARBOHYDRATE("011", "백반, 죽, 국수"), + DOSIRAK("012", "도시락"), + K_SNACK_FOOD("013", "분식"), + CAFE("014", "카페, 디저트"), + BURGER("015", "패스트푸드"); - private final String category; + private final String code; + private final String stringCategory; - public String getCode() { - return category; + // 한글 설명(String)을 ENUM으로 변환 + public static Optional fromStringToEnum(String category) { + return Arrays.stream(Category.values()) + .filter(c -> c.getStringCategory().equals(category)) + .findFirst(); + } + + // ENUM을 코드(String)으로 변환 + public static Optional fromEnumToString(Category category) { + return Optional.of(category.getCode()) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 카테고리입니다.")) + .describeConstable(); // 상수 풀에 저장되는 값을 안전하게 참조 } @jakarta.persistence.Converter(autoApply = true) @@ -33,4 +49,10 @@ public Converter() { super(Category.class); } } + + /* + spotDto.getCategory().getCode() → "015" + spotDto.getCategory().getCategory() → "패스트푸드" + spotDto.getCategory() 자체는 Category.BURGER ENUM 객체를 반환 + */ } diff --git a/src/main/java/com/ordertogether/team14_be/spot/enums/ErrorCode.java b/src/main/java/com/ordertogether/team14_be/spot/enums/ErrorCode.java index a04e708c..6ed2f6f7 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/enums/ErrorCode.java +++ b/src/main/java/com/ordertogether/team14_be/spot/enums/ErrorCode.java @@ -8,7 +8,8 @@ public enum ErrorCode { NOT_FOUND("404", "Not found"), INTERNAL_ERROR("500", "Internal server error"), SPOT_NOT_FOUND("404", "Spot not found"), - NULL_VALUE_NOT_ALLOWED("400", "Null value not allowed"); + NULL_VALUE_NOT_ALLOWED("400", "Null value not allowed"), + NOT_SPOT_MASTER("401", "Not spot master"); private final String code; private final String message; @@ -17,4 +18,12 @@ public enum ErrorCode { this.code = code; this.message = message; } + + // ENUM을 코드(String)으로 변환 + public static String fromEnumToString(ErrorCode errorCode) { + if (errorCode == null) { + throw new IllegalArgumentException("존재하지 않는 코드입니다."); + } + return errorCode.getCode(); + } } diff --git a/src/main/java/com/ordertogether/team14_be/spot/exception/ErrorResponse.java b/src/main/java/com/ordertogether/team14_be/spot/exception/ErrorResponse.java deleted file mode 100644 index 24effc29..00000000 --- a/src/main/java/com/ordertogether/team14_be/spot/exception/ErrorResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.ordertogether.team14_be.spot.exception; - -import com.ordertogether.team14_be.spot.enums.ErrorCode; - - -public record ErrorResponse(String message, ErrorCode code) {} diff --git a/src/main/java/com/ordertogether/team14_be/spot/exception/NotSpotMasterException.java b/src/main/java/com/ordertogether/team14_be/spot/exception/NotSpotMasterException.java new file mode 100644 index 00000000..76208642 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/spot/exception/NotSpotMasterException.java @@ -0,0 +1,15 @@ +package com.ordertogether.team14_be.spot.exception; + +import com.ordertogether.team14_be.spot.enums.ErrorCode; +import lombok.Getter; + +@Getter +public class NotSpotMasterException extends RuntimeException { + + private final ErrorCode errorCode; + + public NotSpotMasterException(String message) { + super(message); + this.errorCode = ErrorCode.NOT_SPOT_MASTER; + } +} diff --git a/src/main/java/com/ordertogether/team14_be/spot/exception/SpotExceptionHandler.java b/src/main/java/com/ordertogether/team14_be/spot/exception/SpotExceptionHandler.java index c987e601..24a5e4fe 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/exception/SpotExceptionHandler.java +++ b/src/main/java/com/ordertogether/team14_be/spot/exception/SpotExceptionHandler.java @@ -3,6 +3,7 @@ import com.ordertogether.team14_be.spot.controller.SpotController; import com.ordertogether.team14_be.spot.enums.ErrorCode; import jakarta.validation.ConstraintViolationException; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -11,16 +12,21 @@ @RestControllerAdvice(assignableTypes = SpotController.class) public class SpotExceptionHandler { - @ExceptionHandler(SpotNotFoundException.class) - public ResponseEntity handleSpotNotFoundException(SpotNotFoundException ex) { - return ResponseEntity.badRequest() - .body(new ErrorResponse(ex.getMessage(), ErrorCode.SPOT_NOT_FOUND)); + @ExceptionHandler({SpotNotFoundException.class, NotSpotMasterException.class}) + public ResponseEntity handleSpotNotFoundException(SpotNotFoundException ex) { + return ResponseEntity.status( + HttpStatus.valueOf(Integer.parseInt(ErrorCode.fromEnumToString(ex.getErrorCode())))) + .body(ex.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleMethodArgumentNotValidException( + public ResponseEntity handleMethodArgumentNotValidException( ConstraintViolationException ex) { - return ResponseEntity.badRequest() - .body(new ErrorResponse(ex.getMessage(), ErrorCode.NULL_VALUE_NOT_ALLOWED)); + return ResponseEntity.badRequest().body(ex.getMessage()); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception ex) { + return ResponseEntity.internalServerError().body(ex.getMessage()); } } diff --git a/src/main/java/com/ordertogether/team14_be/spot/mapper/SpotMapper.java b/src/main/java/com/ordertogether/team14_be/spot/mapper/SpotMapper.java index 283272c0..42764384 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/mapper/SpotMapper.java +++ b/src/main/java/com/ordertogether/team14_be/spot/mapper/SpotMapper.java @@ -3,9 +3,8 @@ import com.ordertogether.team14_be.spot.dto.controllerdto.*; import com.ordertogether.team14_be.spot.dto.servicedto.SpotDto; import com.ordertogether.team14_be.spot.entity.Spot; -import org.mapstruct.Mapper; -import org.mapstruct.MappingTarget; -import org.mapstruct.ReportingPolicy; +import com.ordertogether.team14_be.spot.enums.Category; +import org.mapstruct.*; import org.mapstruct.factory.Mappers; @Mapper( @@ -16,19 +15,35 @@ public interface SpotMapper { SpotDto toDto(Spot spot); + @Mapping(target = "category", ignore = true) // category는 무시 SpotDto toSpotDto(SpotCreationRequest spotCreationRequest); Spot toEntity(SpotDto spotDto); Spot toEntity(SpotDto spotDto, @MappingTarget Spot spot); // 생성 또는 수정할 때 사용 + @BeanMapping(ignoreByDefault = false) // 자동 매핑 활성화 + @Mapping(target = "category", expression = "java(spotDto.getCategory().getStringCategory())") SpotCreationResponse toSpotCreationResponse(SpotDto spotDto); + @BeanMapping(ignoreByDefault = false) // 자동 매핑 활성화 + @Mapping(target = "category", expression = "java(spotDto.getCategory().getStringCategory())") SpotDetailResponse toSpotDetailResponse(SpotDto spotDto); + @BeanMapping(ignoreByDefault = false) // 자동 매핑 활성화 + @Mapping(target = "category", expression = "java(spotDto.getCategory().getStringCategory())") SpotViewedResponse toSpotViewedResponse(SpotDto spotDto); SpotModifyRequest toSpotModifyRequest(SpotDto spotDto); + @Mapping(target = "category", ignore = true) // category는 무시 SpotDto toSpotDto(SpotModifyRequest spotModifyRequest); + + @AfterMapping + default void mapToCategory( + SpotCreationRequest spotCreationRequest, @MappingTarget SpotDto spotDto) { + spotDto.setCategory( + Category.fromStringToEnum(spotCreationRequest.category()) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 카테고리입니다."))); + } } diff --git a/src/main/java/com/ordertogether/team14_be/spot/repository/SimpleSpotRepository.java b/src/main/java/com/ordertogether/team14_be/spot/repository/SimpleSpotRepository.java index f326be19..8cf5f5b5 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/repository/SimpleSpotRepository.java +++ b/src/main/java/com/ordertogether/team14_be/spot/repository/SimpleSpotRepository.java @@ -5,8 +5,6 @@ import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -15,13 +13,5 @@ public interface SimpleSpotRepository extends JpaRepository { Optional findByIdAndIsDeletedFalse(Long id); - @Query( - "SELECT sp FROM Spot sp WHERE sp.lat <= :maxlat and sp.lat >= :minlat and sp.lng <= :maxlng and sp.lng >= :minlng and sp.isDeleted = false") - List findAroundSpotAndIsDeletedFalse( - @Param("maxlat") BigDecimal maxlat, - @Param("maxlng") BigDecimal maxlng, - @Param("minlat") BigDecimal minlat, - @Param("minlng") BigDecimal minlng); - List findByGeoHash(String geoHash); } diff --git a/src/main/java/com/ordertogether/team14_be/spot/service/SpotService.java b/src/main/java/com/ordertogether/team14_be/spot/service/SpotService.java index 509ac61d..d164341f 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/service/SpotService.java +++ b/src/main/java/com/ordertogether/team14_be/spot/service/SpotService.java @@ -6,10 +6,13 @@ import com.ordertogether.team14_be.spot.dto.controllerdto.SpotViewedResponse; import com.ordertogether.team14_be.spot.dto.servicedto.SpotDto; import com.ordertogether.team14_be.spot.entity.Spot; +import com.ordertogether.team14_be.spot.exception.NotSpotMasterException; import com.ordertogether.team14_be.spot.mapper.SpotMapper; import com.ordertogether.team14_be.spot.repository.SpotRepository; import java.math.BigDecimal; +import java.time.LocalDateTime; import java.util.List; +import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,10 +32,14 @@ public List getSpot(BigDecimal lat, BigDecimal lng) { @Transactional public SpotCreationResponse createSpot(SpotDto spotDto) { - BigDecimal lat = spotDto.getLat(); - BigDecimal lng = spotDto.getLng(); - GeoHash geoHash = GeoHash.withCharacterPrecision(lat.doubleValue(), lng.doubleValue(), 12); + GeoHash geoHash = + GeoHash.withCharacterPrecision( + spotDto.getLat().doubleValue(), spotDto.getLng().doubleValue(), 12); spotDto.setGeoHash(geoHash.toBase32()); + spotDto.setCreatedAt(LocalDateTime.now()); + spotDto.setCreatedBy(spotDto.getId()); + spotDto.setModifiedAt(LocalDateTime.now()); + spotDto.setModifiedBy(spotDto.getModifiedBy()); Spot spot = SpotMapper.INSTANCE.toEntity(spotDto, new Spot()); return SpotMapper.INSTANCE.toSpotCreationResponse(spotRepository.save(spot)); } @@ -58,6 +65,10 @@ public List getSpotByGeoHash(BigDecimal lat, BigDecimal lng) @Transactional public SpotDto updateSpot(SpotDto spotDto) { + if (!Objects.equals(spotDto.getId(), spotDto.getCreatedBy())) { + throw new NotSpotMasterException("작성자만 수정할 수 있습니다."); + } + spotDto.setModifiedAt(LocalDateTime.now()); Spot spot = SpotMapper.INSTANCE.toEntity( spotDto, @@ -68,6 +79,11 @@ public SpotDto updateSpot(SpotDto spotDto) { @Transactional public void deleteSpot(Long id) { + // id가 createdBy와 일치하는지 검증 후 delete + SpotDto spotDto = spotRepository.findByIdAndIsDeletedFalse(id); + if (!Objects.equals(spotDto.getCreatedBy(), id)) { + throw new IllegalArgumentException("방장이 아닌 사람은 삭제할 수 없습니다."); + } spotRepository.delete(id); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 249b0064..b2603de4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -13,6 +13,7 @@ spring: properties: hibernate: format_sql: true + dialect: org.hibernate.dialect.MySQL8Dialect defer-datasource-initialization: true sql: diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index b3610722..4b7ecbf0 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,5 +1,26 @@ -INSERT INTO member (id, email, point, phone_number, delivery_name, platform) VALUES (1, 'member1@example.com', 100000, '010-1234-5678', 'John Doe', 'Kakao'); +INSERT INTO member (id, email, point, phone_number, delivery_name, platform) VALUES (1, 'member1@example.com', 1000000, '010-0000-0001', 'Member01', 'Kakao'); +INSERT INTO member (id, email, point, phone_number, delivery_name, platform) VALUES (2, 'member2@example.com', 1000000, '010-0000-0002', 'Member02', 'Kakao'); INSERT INTO product (id, name, price, created_at, modified_at, created_by, modified_by) VALUES (1, 'Product 1', 10000, now(), now(), 1, 1); INSERT INTO product (id, name, price, created_at, modified_at, created_by, modified_by) VALUES (2, 'Product 2', 20000, now(), now(), 1, 1); INSERT INTO product (id, name, price, created_at, modified_at, created_by, modified_by) VALUES (3, 'Product 3', 30000, now(), now(), 1, 1); + +INSERT INTO payment_event (id, buyer_id, created_at, modified_at, order_id, order_name, payment_key, payment_status) VALUES (1, 1, now(), now(), 'test-order-id-1', 'Product 1, Product 2, Product 3', 'test-payment-key-1', 'SUCCESS'); +INSERT INTO payment_order (id, product_id, order_id, order_name, amount, created_at, modified_at) VALUES (1, 1, 'test-order-id-1', 'Product 1', 10000, now(), now()); +INSERT INTO payment_order (id, product_id, order_id, order_name, amount, created_at, modified_at) VALUES (2, 2, 'test-order-id-1', 'Product 2', 20000, now(), now()); +INSERT INTO payment_order (id, product_id, order_id, order_name, amount, created_at, modified_at) VALUES (3, 3, 'test-order-id-1', 'Product 3', 30000, now(), now()); + +INSERT INTO payment_event (id, buyer_id, created_at, modified_at, order_id, order_name, payment_key, payment_status) VALUES (2, 1, now(), now(), 'test-order-id-2', 'Product 1, Product 2', 'test-payment-key-2', 'SUCCESS'); +INSERT INTO payment_order (id, product_id, order_id, order_name, amount, created_at, modified_at) VALUES (4, 1, 'test-order-id-2', 'Product 1', 10000, now(), now()); +INSERT INTO payment_order (id, product_id, order_id, order_name, amount, created_at, modified_at) VALUES (5, 2, 'test-order-id-2', 'Product 2', 20000, now(), now()); + +INSERT INTO payment_event (id, buyer_id, created_at, modified_at, order_id, order_name, payment_key, payment_status) VALUES (3, 1, now(), now(), 'test-order-id-3', 'Product 1, Product 3', 'test-payment-key-3', 'FAIL'); +INSERT INTO payment_order (id, product_id, order_id, order_name, amount, created_at, modified_at) VALUES (6, 1, 'test-order-id-3', 'Product 1', 10000, now(), now()); +INSERT INTO payment_order (id, product_id, order_id, order_name, amount, created_at, modified_at) VALUES (7, 3, 'test-order-id-3', 'Product 3', 30000, now(), now()); + +INSERT INTO payment_event (id, buyer_id, created_at, modified_at, order_id, order_name, payment_key, payment_status) VALUES (4, 2, now(), now(), 'test-order-id-4', 'Product 1', 'test-payment-key-4', 'SUCCESS'); +INSERT INTO payment_order (id, product_id, order_id, order_name, amount, created_at, modified_at) VALUES (8, 1, 'test-order-id-4', 'Product 1', 10000, now(), now()); + +INSERT INTO payment_event (id, buyer_id, created_at, modified_at, order_id, order_name, payment_key, payment_status) VALUES (5, 2, now(), now(), 'test-order-id-5', 'Product 1, Product 2', 'test-payment-key-5', 'SUCCESS'); +INSERT INTO payment_order (id, product_id, order_id, order_name, amount, created_at, modified_at) VALUES (9, 1, 'test-order-id-5', 'Product 1', 10000, now(), now()); +INSERT INTO payment_order (id, product_id, order_id, order_name, amount, created_at, modified_at) VALUES (10, 2, 'test-order-id-5', 'Product 2', 20000, now(), now()); diff --git a/src/test/java/com/ordertogether/team14_be/helper/PaymentDatabaseHelper.java b/src/test/java/com/ordertogether/team14_be/helper/PaymentDatabaseHelper.java index c8b44021..298ccb1f 100644 --- a/src/test/java/com/ordertogether/team14_be/helper/PaymentDatabaseHelper.java +++ b/src/test/java/com/ordertogether/team14_be/helper/PaymentDatabaseHelper.java @@ -5,6 +5,4 @@ public interface PaymentDatabaseHelper { void clean(); void saveTestData(); - - void setOrderId(String orderId); } diff --git a/src/test/java/com/ordertogether/team14_be/helper/jpa/JpaPaymentDatabaseHelper.java b/src/test/java/com/ordertogether/team14_be/helper/jpa/JpaPaymentDatabaseHelper.java index c2b6da2e..2ff09a27 100644 --- a/src/test/java/com/ordertogether/team14_be/helper/jpa/JpaPaymentDatabaseHelper.java +++ b/src/test/java/com/ordertogether/team14_be/helper/jpa/JpaPaymentDatabaseHelper.java @@ -12,7 +12,6 @@ import com.ordertogether.team14_be.payment.persistence.repository.ProductRepository; import java.math.BigDecimal; import java.util.List; -import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -27,8 +26,6 @@ public class JpaPaymentDatabaseHelper implements PaymentDatabaseHelper { private final ProductRepository productRepository; private final MemberRepository memberRepository; - private String orderId; - @Override public void clean() { jpaDatabaseCleanup.execute(); @@ -37,56 +34,158 @@ public void clean() { @Override @Transactional public void saveTestData() { - if (Objects.isNull(orderId)) { - throw new IllegalStateException("orderId is not set"); - } - memberRepository.save( - new Member(1L, "member1@example.com", 100000, "010-1234-5678", "member1", "Kakao")); + // Save members + memberRepository.saveAll( + List.of( + new Member(1L, "member1@example.com", 1000000, "010-0000-0001", "Member01", "Kakao"), + new Member(2L, "member2@example.com", 1000000, "010-0000-0002", "Member02", "Kakao"))); + // Save products productRepository.saveAll( List.of( Product.builder().id(1L).name("Product 1").price(BigDecimal.valueOf(10000)).build(), Product.builder().id(2L).name("Product 2").price(BigDecimal.valueOf(20000)).build(), Product.builder().id(3L).name("Product 3").price(BigDecimal.valueOf(30000)).build())); - List paymentOrders = + // Save payment orders and events + List paymentOrders1 = paymentOrderRepository.saveAll( List.of( PaymentOrder.builder() .id(1L) .productId(1L) - .orderId(orderId) + .orderId("test-order-id-1") .orderName("Product 1") - .amount(BigDecimal.valueOf(10000L)) + .amount(BigDecimal.valueOf(10000)) .build(), PaymentOrder.builder() .id(2L) .productId(2L) - .orderId(orderId) + .orderId("test-order-id-1") .orderName("Product 2") - .amount(BigDecimal.valueOf(20000L)) + .amount(BigDecimal.valueOf(20000)) .build(), PaymentOrder.builder() .id(3L) .productId(3L) - .orderId(orderId) + .orderId("test-order-id-1") .orderName("Product 3") - .amount(BigDecimal.valueOf(30000L)) + .amount(BigDecimal.valueOf(30000)) .build())); paymentEventRepository.save( PaymentEvent.builder() .id(1L) .buyerId(1L) - .orderId(orderId) - .paymentOrders(paymentOrders) + .orderId("test-order-id-1") + .paymentOrders(paymentOrders1) .orderName("Product 1, Product 2, Product 3") .paymentStatus(PaymentStatus.READY) .build()); - } - @Override - public void setOrderId(String orderId) { - this.orderId = orderId; + List paymentOrders2 = + paymentOrderRepository.saveAll( + List.of( + PaymentOrder.builder() + .id(4L) + .productId(1L) + .orderId("test-order-id-2") + .orderName("Product 1") + .amount(BigDecimal.valueOf(10000)) + .build(), + PaymentOrder.builder() + .id(5L) + .productId(2L) + .orderId("test-order-id-2") + .orderName("Product 2") + .amount(BigDecimal.valueOf(20000)) + .build())); + + paymentEventRepository.save( + PaymentEvent.builder() + .id(2L) + .buyerId(1L) + .orderId("test-order-id-2") + .paymentOrders(paymentOrders2) + .orderName("Product 1, Product 2") + .paymentStatus(PaymentStatus.SUCCESS) + .build()); + + List paymentOrders3 = + paymentOrderRepository.saveAll( + List.of( + PaymentOrder.builder() + .id(6L) + .productId(1L) + .orderId("test-order-id-3") + .orderName("Product 1") + .amount(BigDecimal.valueOf(10000)) + .build(), + PaymentOrder.builder() + .id(7L) + .productId(3L) + .orderId("test-order-id-3") + .orderName("Product 3") + .amount(BigDecimal.valueOf(30000)) + .build())); + + paymentEventRepository.save( + PaymentEvent.builder() + .id(3L) + .buyerId(1L) + .orderId("test-order-id-3") + .paymentOrders(paymentOrders3) + .orderName("Product 1, Product 3") + .paymentStatus(PaymentStatus.FAIL) + .build()); + + List paymentOrders4 = + paymentOrderRepository.saveAll( + List.of( + PaymentOrder.builder() + .id(8L) + .productId(1L) + .orderId("test-order-id-4") + .orderName("Product 1") + .amount(BigDecimal.valueOf(10000)) + .build())); + + paymentEventRepository.save( + PaymentEvent.builder() + .id(4L) + .buyerId(2L) + .orderId("test-order-id-4") + .paymentOrders(paymentOrders4) + .orderName("Product 1") + .paymentStatus(PaymentStatus.SUCCESS) + .build()); + + List paymentOrders5 = + paymentOrderRepository.saveAll( + List.of( + PaymentOrder.builder() + .id(9L) + .productId(1L) + .orderId("test-order-id-5") + .orderName("Product 1") + .amount(BigDecimal.valueOf(10000)) + .build(), + PaymentOrder.builder() + .id(10L) + .productId(2L) + .orderId("test-order-id-5") + .orderName("Product 2") + .amount(BigDecimal.valueOf(20000)) + .build())); + + paymentEventRepository.save( + PaymentEvent.builder() + .id(5L) + .buyerId(2L) + .orderId("test-order-id-5") + .paymentOrders(paymentOrders5) + .orderName("Product 1, Product 2") + .paymentStatus(PaymentStatus.SUCCESS) + .build()); } } diff --git a/src/test/java/com/ordertogether/team14_be/payment/service/PaymentConfirmServiceTest.java b/src/test/java/com/ordertogether/team14_be/payment/service/PaymentConfirmServiceTest.java index b5543afd..ced0c5f9 100644 --- a/src/test/java/com/ordertogether/team14_be/payment/service/PaymentConfirmServiceTest.java +++ b/src/test/java/com/ordertogether/team14_be/payment/service/PaymentConfirmServiceTest.java @@ -57,8 +57,6 @@ void setUp() { pointManagementService); paymentDatabaseHelper.clean(); - - paymentDatabaseHelper.setOrderId("test-order-id"); paymentDatabaseHelper.saveTestData(); } @@ -75,7 +73,7 @@ void shouldSaveSuccessStatusWhenNormallyRequest() { long chargeAmount = 60000L; PaymentConfirmRequest request = PaymentConfirmRequest.builder() - .orderId("test-order-id") + .orderId("test-order-id-1") .paymentKey(UUID.randomUUID().toString()) .amount(chargeAmount) .build(); diff --git a/src/test/java/com/ordertogether/team14_be/payment/service/PaymentValidationServiceTest.java b/src/test/java/com/ordertogether/team14_be/payment/service/PaymentValidationServiceTest.java index a42c4c18..ece4f4db 100644 --- a/src/test/java/com/ordertogether/team14_be/payment/service/PaymentValidationServiceTest.java +++ b/src/test/java/com/ordertogether/team14_be/payment/service/PaymentValidationServiceTest.java @@ -1,3 +1,4 @@ +/* package com.ordertogether.team14_be.payment.service; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -82,3 +83,6 @@ void shouldThrowExceptionWhenRequestWithInvalidOrderId() { .isInstanceOf(NoSuchElementException.class); } } + + + */ diff --git a/src/test/java/com/ordertogether/team14_be/payment/service/PointManagementServiceTest.java b/src/test/java/com/ordertogether/team14_be/payment/service/PointManagementServiceTest.java index 9daadf15..086f35bb 100644 --- a/src/test/java/com/ordertogether/team14_be/payment/service/PointManagementServiceTest.java +++ b/src/test/java/com/ordertogether/team14_be/payment/service/PointManagementServiceTest.java @@ -27,8 +27,6 @@ class PointManagementServiceTest { @BeforeEach void setUp() { paymentDatabaseHelper.clean(); - - paymentDatabaseHelper.setOrderId("test-order-id"); paymentDatabaseHelper.saveTestData(); } @@ -43,12 +41,12 @@ void shouldIncreaseSuccessWhenNormallyRequest() { .getPoint(); Long chargeAmount = paymentEventRepository - .findByOrderId("test-order-id") + .findByOrderId("test-order-id-1") .orElseThrow(() -> new NoSuchElementException("PaymentEvent not found")) .totalAmount(); // when - pointManagementService.increasePoint("test-order-id"); + pointManagementService.increasePoint("test-order-id-1"); // then int afterPoint = diff --git a/src/test/java/com/ordertogether/team14_be/spot/unit/SpotRepositoryTest.java b/src/test/java/com/ordertogether/team14_be/spot/unit/SpotRepositoryTest.java new file mode 100644 index 00000000..73490d23 --- /dev/null +++ b/src/test/java/com/ordertogether/team14_be/spot/unit/SpotRepositoryTest.java @@ -0,0 +1,130 @@ +package com.ordertogether.team14_be.spot.unit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +import com.ordertogether.team14_be.spot.dto.servicedto.SpotDto; +import com.ordertogether.team14_be.spot.entity.Spot; +import com.ordertogether.team14_be.spot.enums.Category; +import com.ordertogether.team14_be.spot.exception.SpotNotFoundException; +import com.ordertogether.team14_be.spot.mapper.SpotMapper; +import com.ordertogether.team14_be.spot.repository.SimpleSpotRepository; +import com.ordertogether.team14_be.spot.repository.SpotRepository; +import java.math.BigDecimal; +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SpotRepositoryTest { + + @InjectMocks private SpotRepository spotRepository; + + @Mock private SimpleSpotRepository simpleSpotRepository; + + @Spy private SpotMapper spotMapper; + + private BigDecimal lat; + private BigDecimal lng; + private Spot spot; + + @BeforeEach + void setUp() { + lat = new BigDecimal("37.7749"); + lng = new BigDecimal("-122.4194"); + + // SpotDto 초기화 + spot = + Spot.builder() + .id(1L) + .lat(lat) + .lng(lng) + .id(1L) + .category(Category.BURGER) + .storeName("맥도날드") + .minimumOrderAmount(12000) + .deadlineTime(LocalTime.of(12, 0, 0)) + .pickUpLocation("픽업위치") + .createdBy(1L) + .build(); + } + + @Test + void save_success() { + when(simpleSpotRepository.save(spot)).thenReturn(spot); + + SpotDto savedSpot = spotRepository.save(spot); + assertThat(savedSpot.getId()).isEqualTo(spot.getId()); + assertThat(savedSpot.getLat()).isEqualTo(spot.getLat()); + assertThat(savedSpot.getLng()).isEqualTo(spot.getLng()); + assertThat(savedSpot.getCategory()).isEqualTo(spot.getCategory()); + assertThat(savedSpot.getStoreName()).isEqualTo(spot.getStoreName()); + assertThat(savedSpot.getMinimumOrderAmount()).isEqualTo(spot.getMinimumOrderAmount()); + assertThat(savedSpot.getDeadlineTime()).isEqualTo(spot.getDeadlineTime()); + assertThat(savedSpot.getPickUpLocation()).isEqualTo(spot.getPickUpLocation()); + assertThat(savedSpot.getCreatedBy()).isEqualTo(spot.getCreatedBy()); + } + + @Test + void findByIdAndIsDeletedFalse_success() { + when(simpleSpotRepository.findByIdAndIsDeletedFalse(1L)).thenReturn(Optional.of(spot)); + + SpotDto spotDto = spotRepository.findByIdAndIsDeletedFalse(1L); + assertThat(spotDto.getId()).isEqualTo(1L); + assertThat(spotDto.getLat()).isEqualTo(lat); + assertThat(spotDto.getLng()).isEqualTo(lng); + assertThat(spotDto.getCategory()).isEqualTo(Category.BURGER); + assertThat(spotDto.getStoreName()).isEqualTo("맥도날드"); + assertThat(spotDto.getMinimumOrderAmount()).isEqualTo(12000); + assertThat(spotDto.getDeadlineTime()).isEqualTo(LocalTime.of(12, 0, 0)); + assertThat(spotDto.getPickUpLocation()).isEqualTo("픽업위치"); + assertThat(spotDto.getCreatedBy()).isEqualTo(1L); + } + + @Test + void findByIdAndIsDeletedFalse_exception() { + when(simpleSpotRepository.findByIdAndIsDeletedFalse(1L)).thenReturn(Optional.empty()); + + assertThrows(SpotNotFoundException.class, () -> spotRepository.findByIdAndIsDeletedFalse(1L)); + } + + @Test + void findByLatAndLngAndIsDeletedFalse_success() { + when(simpleSpotRepository.findByLatAndLngAndIsDeletedFalse(lat, lng)).thenReturn(List.of(spot)); + + List spotDto = spotRepository.findByLatAndLngAndIsDeletedFalse(lat, lng); + assertThat(spotDto.getFirst().getId()).isEqualTo(1L); + assertThat(spotDto.getFirst().getLat()).isEqualTo(lat); + assertThat(spotDto.getFirst().getLng()).isEqualTo(lng); + assertThat(spotDto.getFirst().getCategory()).isEqualTo(Category.BURGER); + assertThat(spotDto.getFirst().getStoreName()).isEqualTo("맥도날드"); + assertThat(spotDto.getFirst().getMinimumOrderAmount()).isEqualTo(12000); + assertThat(spotDto.getFirst().getDeadlineTime()).isEqualTo(LocalTime.of(12, 0, 0)); + assertThat(spotDto.getFirst().getPickUpLocation()).isEqualTo("픽업위치"); + assertThat(spotDto.getFirst().getCreatedBy()).isEqualTo(1L); + } + + @Test + void findBygeoHash_success() { + when(simpleSpotRepository.findByGeoHash("9q8yyz")).thenReturn(List.of(spot)); + + List spotDto = spotRepository.findBygeoHash("9q8yyz"); + assertThat(spotDto.getFirst().getId()).isEqualTo(1L); + assertThat(spotDto.getFirst().getLat()).isEqualTo(lat); + assertThat(spotDto.getFirst().getLng()).isEqualTo(lng); + assertThat(spotDto.getFirst().getCategory()).isEqualTo(Category.BURGER); + assertThat(spotDto.getFirst().getStoreName()).isEqualTo("맥도날드"); + assertThat(spotDto.getFirst().getMinimumOrderAmount()).isEqualTo(12000); + assertThat(spotDto.getFirst().getDeadlineTime()).isEqualTo(LocalTime.of(12, 0, 0)); + assertThat(spotDto.getFirst().getPickUpLocation()).isEqualTo("픽업위치"); + assertThat(spotDto.getFirst().getCreatedBy()).isEqualTo(1L); + } +} diff --git a/src/test/java/com/ordertogether/team14_be/spot/unit/SpotServiceTest.java b/src/test/java/com/ordertogether/team14_be/spot/unit/SpotServiceTest.java new file mode 100644 index 00000000..4048321a --- /dev/null +++ b/src/test/java/com/ordertogether/team14_be/spot/unit/SpotServiceTest.java @@ -0,0 +1,131 @@ +package com.ordertogether.team14_be.spot.unit; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import ch.hsr.geohash.GeoHash; +import com.ordertogether.team14_be.spot.dto.controllerdto.SpotCreationResponse; +import com.ordertogether.team14_be.spot.dto.controllerdto.SpotViewedResponse; +import com.ordertogether.team14_be.spot.dto.servicedto.SpotDto; +import com.ordertogether.team14_be.spot.entity.Spot; +import com.ordertogether.team14_be.spot.enums.Category; +import com.ordertogether.team14_be.spot.exception.NotSpotMasterException; +import com.ordertogether.team14_be.spot.repository.SpotRepository; +import com.ordertogether.team14_be.spot.service.SpotService; +import java.math.BigDecimal; +import java.time.LocalTime; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SpotServiceTest { + + @Mock private SpotRepository spotRepository; + + @InjectMocks private SpotService spotService; + + private BigDecimal lat; + private BigDecimal lng; + private SpotDto spotDto; + + @BeforeEach + void setUp() { + lat = new BigDecimal("37.7749"); + lng = new BigDecimal("-122.4194"); + + // SpotDto 초기화 + spotDto = + SpotDto.builder() + .id(1L) + .lat(lat) + .lng(lng) + .id(1L) + .category(Category.BURGER) + .storeName("맥도날드") + .minimumOrderAmount(12000) + .deadlineTime(LocalTime.of(12, 0, 0)) + .pickUpLocation("픽업위치") + .createdBy(1L) + .build(); + } + + @Test + void getSpot_success() { + when(spotRepository.findByLatAndLngAndIsDeletedFalse(lat, lng)).thenReturn(List.of(spotDto)); + List result = spotService.getSpot(lat, lng); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("패스트푸드", result.getFirst().category()); + assertEquals("맥도날드", result.getFirst().storeName()); + assertEquals(12000, result.getFirst().minimumOrderAmount()); + assertEquals("픽업위치", result.getFirst().pickUpLocation()); + verify(spotRepository).findByLatAndLngAndIsDeletedFalse(lat, lng); + } + + @Test + void createSpot_success() { + when(spotRepository.save(any(Spot.class))).thenReturn(spotDto); + + SpotCreationResponse response = spotService.createSpot(spotDto); + + assertNotNull(response); + System.out.println(response); + assertEquals(spotDto.getId(), response.id()); + assertEquals(spotDto.getCategory().getStringCategory(), response.category()); + assertEquals(spotDto.getStoreName(), response.storeName()); + assertEquals(spotDto.getMinimumOrderAmount(), response.minimumOrderAmount()); + assertEquals(spotDto.getPickUpLocation(), response.pickUpLocation()); + assertEquals(spotDto.getDeadlineTime(), response.deadlineTime()); + + verify(spotRepository).save(any(Spot.class)); + } + + @Test + void getSpotByGeoHash_success() { + GeoHash geoHash = GeoHash.withCharacterPrecision(lat.doubleValue(), lng.doubleValue(), 12); + String geoHashString = geoHash.toBase32(); + + when(spotRepository.findBygeoHash(geoHashString)).thenReturn(List.of(spotDto)); + + List result = spotService.getSpotByGeoHash(lat, lng); + + assertFalse(result.isEmpty()); + verify(spotRepository).findBygeoHash(geoHashString); + } + + @Test + void updateSpot_throwsNotSpotMasterException() { + SpotDto spotDtoWithDifferentCreator = SpotDto.builder().id(1L).createdBy(2L).build(); + assertThrows( + NotSpotMasterException.class, () -> spotService.updateSpot(spotDtoWithDifferentCreator)); + } + + @Test + void deleteSpot_success() { + // given + Long id = 1L; + when(spotRepository.findByIdAndIsDeletedFalse(id)).thenReturn(spotDto); + + // when + spotService.deleteSpot(id); + + // then + verify(spotRepository, times(1)).delete(id); + } + + @Test + void deleteSpot_throwsIllegalArgumentException() { + Long id = 1L; + SpotDto exceptionSpotDto = SpotDto.builder().id(1L).createdBy(2L).build(); + when(spotRepository.findByIdAndIsDeletedFalse(id)).thenReturn(exceptionSpotDto); + + assertThrows(IllegalArgumentException.class, () -> spotService.deleteSpot(id)); + } +} From 5c7cd24e2f96340a707bbbd68a5bbbb8e0e3e5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EC=A0=9C=EB=B2=95?= <89574219+nove1080@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:57:32 +0900 Subject: [PATCH 3/3] Update README.md --- README.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/README.md b/README.md index 64740ff6..f10cbbff 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,82 @@ # Team14_BE + 14조 백엔드 + +## 프로젝트 소개 +- **프로젝트 명**: 요기먹때 +- **프로젝트 기간**: 24.09 ~ 24.11 +- **프로젝트 목표**: 높아진 배달 요구 금액에 대한 부담을 모르는 사람과 함께 주문하여 해결한다. + +> 모르는 사람과 함께 배달을 시켜보세요! + +![image](https://github.com/user-attachments/assets/0d40077d-5791-4a3d-981d-8c2726c2083f) + +- 🙋🏻‍♀️배달 가격이 부담스러운 사람을 대상으로 +- 👩‍👦함께 배달 음식을 주문할 수 있는 사람을 매칭해 +- 💵배달 최소 주문 금액 부담을 덜고 추가적인 배달음식비 또는 배달팁을 줄여보세요! + +## 배포 링크 + +✅FE: https://team14-fe.vercel.app/ +✅BE: https://order-together.duckdns.org/api/v1 + +## 주요기능 + +### 1. 주문 결제(토스 페이먼츠 API) +* 토스페이먼츠 API 를 사용한 포인트 충전 시스템 +* 요기먹때에서 사용되는 재화를 충전할 수 있습니다. + + +## ERD 이미지 + +![image](https://github.com/user-attachments/assets/59b8b750-ceb3-4484-a538-acd53d7fc370) + +## 개발 인원 : 7명 + +| 이름 | 담당 역할 및 기능 | +| ------ | ---------------------------------------------------------------------- | +| 강호정 | 마이페이지 | +| 서민지 | 스팟(메인)페이지 | +| 임지환 | 로그인 및 결제페이지 | +| 나제법 | 결제 API | +| 서영우 | 로그인 API, 회원 API | +| 안재영 | SMS API, 결제내역 API | +| 유보민 | 지도(스팟) API | + +## ⚒️기술 스택 +### Language +* Java 21 +### Framework +* Spring Boot 3.3.3 +* Spring Data JPA +### Database +* MySQL 8.0 +### Infra +* Amazon Web Service +### Testing +* JUnit5 +* Mockito + +## 💻UI +
+

로그인 UI

+ +![image](https://github.com/user-attachments/assets/21048e34-2189-4c7c-b5a7-a08a72d794fa) +![image](https://github.com/user-attachments/assets/c01f8c58-8d28-4794-9fd0-562888dc5abf) + +
+ +
+

근처 함께 주문 건 확인

+ +![image](https://github.com/user-attachments/assets/fc30c84e-6976-4aa9-a01b-41139462df23) + +
+ +
+

함께 주문하기 생성

+ +![image](https://github.com/user-attachments/assets/c0f54d1b-c3bd-4a89-a938-087fd83d4e63) +![image](https://github.com/user-attachments/assets/5d86ad01-ba0f-4050-ac64-b367dd45528b) + +