diff --git a/.github/workflows/shimpyoDeploy.yml b/.github/workflows/shimpyoDeploy.yml index 1e62321e..b3e1f029 100644 --- a/.github/workflows/shimpyoDeploy.yml +++ b/.github/workflows/shimpyoDeploy.yml @@ -23,6 +23,7 @@ jobs: - name: Make application.properties run: | + echo "${{ secrets.DEFAULT_PROPERTIES }}" | base64 --decode > src/main/resources/application.yml echo "${{ secrets.LOCAL_PROPERTIES }}" | base64 --decode > src/main/resources/application-local.yml echo "${{ secrets.PROD_PROPERTIES }}" | base64 --decode > src/main/resources/application-prod.yml echo "${{ secrets.TEST_PROPERTIES }}" | base64 --decode > src/test/resources/application.yml diff --git a/.gitignore b/.gitignore index 66ffcb90..ae2f6338 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ build/ .settings .springBeans .sts4-cache -bin/ +bin/git !**/src/main/**/bin/ !**/src/test/**/bin/ @@ -37,7 +37,8 @@ out/ .vscode/ ### Application.yaml ### -**/*.yaml +**/application*.yaml +**/application*.yml ### REST Docs Html ### **/*.html diff --git a/README.md b/README.md index 2f640d95..50eef287 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,29 @@ -# Shimpyo_BE : 숙박 예약 서비스 - -2023-11-20 ~ 2023-12-01 +# Shimpyo_BE : 숙박 정보 조회/검색 및 숙박 예약 시스템 --- -## 📌 목차 +## 0. 목차 -- [멤버](#멤버) -- [설정](#설정) -- [설계](#설계) - - [아키텍처](#아키텍처) - - [DB 설계](#DB-설계) - - [API 설계](#API-설계) -- [CI/CD](#CICD) -- [API 문서](#API-문서) +- [1. 프로젝트 설명](#1-프로젝트-설명) + - [🧑🏻‍💻 팀원](#-팀원) + - [💁🏻 소개](#-소개) + - [🎯 목적](#-목적) + - [🔗 배포 링크](#-배포-링크) +- [2. 설정](#2-설정) +- [3. 설계](#3-설계) + - [📰 아키텍처](#-아키텍처) + - [💾 DB 설계](#-DB-설계) + - [📡 API 설계](#-API-설계) +- [4. CI/CD](#4-cicd) +- [5. 에러 해결](#5-에러-해결) +- [6. 회고](#6-회고) +- [7. API 문서](#7-API-문서) --- -## 멤버 +## 1. 프로젝트 설명 + +### 🧑🏻‍💻 팀원 @@ -42,22 +48,41 @@ + + + + +
정의정
- Backend + BE 팀장
심재철
- Backend + BE 팀원
이주연
- Backend + BE 팀원
+ - CI
+ - REST Docs
+ - 회원
+ - 즐겨찾기
+
+ - CD
+ - EC2
+ - 숙소
+ - 장바구니
+
+ - Redis 분산락
+ - 예약
+ - 별점
+
@@ -68,12 +93,12 @@
- 정지오 프로필 + 백상원 프로필 - 백상원 프로필 + 정지오 프로필 @@ -92,40 +117,128 @@ 최우혁
- Frontend + FE 팀장(전)
- - 정지오
- Frontend +
+ 백상원
+ FE 팀장
- - 백상원
- Frontend +
+ 정지오
+ FE 팀원
장수빈
- Frontend + FE 팀원
소유나
- Frontend + FE 팀원

+### 💁🏻 소개 + +![](src/main/resources/image/main-page.png) +> 숙박 정보 조회/검색 및 숙박 예약 시스템 + +### 🎯 목적 + +- 숙박 시설 제공 및 검색: 사용자가 특정 지역이나 날짜에 필요한 숙박 시설을 쉽게 찾을 수 있도록 정보를 제공합니다. +- 가용한 숙소 정보 제공: 사용자에게 해당 지역의 여러 숙소 옵션, 가격, 시설, 평가 등 다양한 정보를 제공하여 선택을 돕습니다. +- 온라인 예약 및 결제: 사용자가 선택한 숙소를 신속하게 예약하고, 온라인으로 결제할 수 있는 편리한 기능을 제공합니다. +- 평가 제공: 다른 이용자들의 평가를 통해 사용자가 숙소를 선택할 때 도움을 받을 수 있도록 합니다. + +### ⏰ 개발 기간 + +2023-11-20 ~ 2023-12-01 + +### 🔗 배포 링크 + +- BE: https://43.202.234.108.nip.io/ +- FE: https://shimpyo.netlify.app/ + +### 🪄 실행 가이드 + +1. Clone 받는다. + +```shell +git clone https://github.com/Shimpyo-House/Shimpyo_BE.git +cd Shimpyo_BE +``` + +2. 설정 파일을 작성한다. + +> `application.yaml` +> +> ```java +> spring: +> profiles: +> active: local + +> `application-local.yaml` +> ```java +> spring: +> config: +> activate: +> on-profile: local +> datasource: +> driver-class-name: org.mariadb.jdbc.Driver +> url: jdbc:mariadb://localhost:3308/shimpyo?createDatabaseIfNotExist=true&serverTimezone=Asia/Seoul +> username: ${DB_USERNAME} +> password: ${DB_PASSWORD} +> jpa: +> hibernate: +> ddl-auto: update +> show-sql: true +> properties: +> hibernate: +> default_batch_fetch_size: 100 +> data: +> redis: +> host: localhost +> port: 6379 +> jwt: +> secret: ${JWT_SECRET} +> access-token-expire-time: ${ACCEESS_TOKEN_EXPIRE_TIME} +> refresh-token-expire-time: ${REFRESH_TOKEN_EXPIRE_TIME} +> open-api: +> service-key: F+zEWUXK0YK5FtuiHBJpOdqfmCV5qNXt+0F5g7X67//VQnlWbQCMUF5UlKPMetolbj9LAbfC0o7+XBe2AyzaWQ== +> logging: +> level: +> root: debug + +3. Docker 컨테이너 실행 + +```shell +docker-compose up --build -d +``` + +4. Run + +```shell +gradle test +gradle build +gradle bootJar +``` + +5. 테스트 계정 +- EMAIL: test@mail.com +- PW: qwer1234!! --- -## 설정 +## 2. 설정 - 자바 버전: 17 - 스프링 버전: 6.0.13 @@ -140,60 +253,213 @@ - Spring Web - Test Containers - Json -- `applicaion-local.yaml`, `application-prod.yaml`, `.env` 파일은 LMS에서 확인하실 수 있습니다! +- `application.yaml`, `applicaion-local.yaml`, `application-prod.yaml`, `.env` 파일은 LMS에서 확인하실 수 + 있습니다! --- -## 설계 +## 3. 설계 -### 아키텍처 +### 📰 아키텍처 > ![](src/main/resources/image/architecture.png) -### DB 설계 +### 💾 DB 설계 + `ERD` > ![](src/main/resources/image/erd.png) -### API 설계 +### 📡 API 설계 [Spring REST Docs](#API-문서)를 통해 확인하실 수 있습니다. --- -## CI/CD +## 4. CI/CD -### CI +### 🛠️ CI > ![](src/main/resources/image/ci.png) -### CD +### 🛠️ CD > ![](src/main/resources/image/cd.png) --- -## API 문서 +## 5. 에러 해결 + +### 📌 주요 에러 + +> #### < Spring REST Docs snippet 에러 > +> Spring REST Docs 으로 API를 문서화 하기 위해 asciidoc을 사용합니다. +> +> 그런데 테스트 코드를 작성하고 빌드를 수행하면 만들어지는 html을 확인해보니, **snippets 파일을 찾지 못 하는 버그**가 발견되었습니다. +> +> ![](src/main/resources/image/docs-error.png) +> ```shell +> 11월 27, 2023 7:22:00 오후 uri:classloader:/gems/asciidoctor-2.0.10/lib/asciidoctor/reader.rb preprocess_include_directive +> SEVERE: member-api.adoc: line 15: include file not found: C:/Users/jeong/Desktop/FC/Shimpyo_BE/{snippets}/auth-rest-controller-docs-test/sign-up/http-request.adoc +> include file not found: C:/Users/jeong/Desktop/FC/Shimpyo_BE/{snippets}/auth-rest-controller-docs-test/sign-up/http-request.adoc :: member-api.adoc :: C:/Users/jeong/Desktop/FC/Shimpyo_BE/C:/Users/jeong/Desktop/FC/Shimpyo_BE/src/docs/asciidoc/member/member-api.adoc:15 (uri:classloader:/gems/asciidoctor-2.0.10/lib/asciidoctor/reader.rb:preprocess_include_directive) +> ``` +> #### < 에러 해결 > +> snippets 경로를 index.adoc 파일이 아니라 member-api.adoc 파일에서 지정해보니, 정상적으로 작동하는 것을 확인할 수 있었습니다. +> +> ```shell +> ifndef::snippets[] +> :snippets: build/generated-snippets +> endif::[] +> +> = Member REST API Docs +> :doctype: book +> :icons: font +> :source-highlighter: highlightjs +> :toc: left +> :toclevels: 2 +> +> ~~ 생략 ~~ +> ``` + +> #### < Open API Service Key 에러 > +> Open API 에서 숙박 상품 조회를 하기 위해 요청을 보냈는데, 다음과 같은 응답을 받았습니다. +> +> ```shell +> +> +> SEVICE ERROR +> SERVICE_KEY_IS_NOT_REGISTERED_ERROR +> 30 +> +> +> ``` +> Service Key가 등록되지 않았다는 에러 응답이었습니다. +> +> 디코딩 서비스 키를 사용해보기도 하고 인코딩 서비스키를 사용해보기도 했으나 여전히 같은 응답이 돌아왔습니다. +> +> #### < 에러 해결 > +> Service 키를 먼저 인코딩 한 후 Uri 빌드 시 true 옵션을 부여하여 해결했습니다. +> ```java +> // ~~ 생략 ~~ +> private String makeBaseSearchUrl() { +> String STAY_SEARCH_URI = "/searchStay1"; +> return BASE_URL + STAY_SEARCH_URI + +> "?serviceKey=" + URLEncoder.encode(SERVICE_KEY, StandardCharsets.UTF_8) + +> DEFAULT_QUERY_PARAMS; +> } +> // ~~생략~~ +> private JSONObject getAccommodation(int pageSize, int pageNum) throws JSONException { +> URI uri = UriComponentsBuilder.fromHttpUrl(makeBaseSearchUrl()) +> .queryParam("pageNo", pageNum) +> .queryParam("numOfRows", pageSize) +> .build(true).toUri(); +> ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, +> httpEntity, String.class); +> log.info("숙박 정보 조회"); +> return new JSONObject(response.getBody()) +> .getJSONObject("response") +> .getJSONObject("body"); +> } +> // ~~ 생략 ~~ + +> #### < 상수화 Wrapper Type 에러 > +> 상수 조건 값을 Wrapper Type으로 선언해 빌드시 롬복 자체 식별이 안되는 에러가 발생했습니다. +> +> ![](src/main/resources/image/wrapper-error-1.png) +> +> #### < 에러 해결 > +> 유효성 검증 값의 경우 원시 타입으로 선언되어 있어 원시 타입으로 변경하여 해결했습니다. +> +> ![](src/main/resources/image/wrapper-error-2.png) +> + +## 6. 회고 +
+정의정 +
+ +- **느낀 점** + - 본 프로젝트에서 BE 팀장으로서 참여하였는데, 전체적인 프로젝트 총괄을 맡으면서 각 팀간 소통과 회의 진행에 있어서 어려움을 느꼈습니다. + - 이번 협업 경험을 통해 다른 분야와의 소통 방식에 대해 고민하고, 원활한 소통을 위해 서로의 이해와 배려가 중요하다는 것을 다시 한 번 느낄 수 있었습니다. + +- **프로젝트를 하면서 잘했던 점** + - 매일 Daliy Scrum을 진행하여 현재 프로젝트 진행 상황을 파악하고 효과적으로 일 단위 task를 수행할 수 있었습니다. + - 2주 단위 스프린트로 기능을 개발하여 짧은 시간 안에 효율적인 개발이 가능했습니다. + - 적극적인 코드 리뷰를 통해 더 나은 코드 작성법에 대해 생각할 수 있었고, 팀원 간 코드를 분석하고 배울 수 있었습니다. + - GitHub와 Discord를 웹훅을 통해 연결하여 PR, ISSUE 등 알림 기능을 구현하여 빠른 피드백이 가능했습니다. + - REST Docs를 도입하여 테스트 코드를 통한 안정적인 API 문서화를 수행할 수 있었습니다. + +- **프로젝트를 진행하면서 힘들었던 점** + - FE와 개발 속도를 맞추는데 어려움을 느꼈습니다. + - 본 프로젝트는 FE 5명, BE 3명으로, BE가 비교적 적은 인원이었습니다. + - 요구사항과 일정에 맞게 개발을 하면서 FE 팀이 API 연결 기간을 충분히 가질 수 있도록 하기 위해 많은 노력이 필요했습니다. + - 또한, 서로의 기술에 대해 모르다보니 FE와의 소통에 어려움을 느꼈습니다. + - FE와의 협업을 위한 기본적인 지식을 겸비해야겠다는 생각을 했습니다. + +
+
+ +
+이주연 +
+ +- **느낀 점** + - 프론트엔드와의 협업 프로젝트는 처음이었는데, 프론트엔드와 소통하며 API를 연결하는 방법을 배울 수 있었던 시간이었습니다. + - 매일 10시, 17시 데일리 스크럼 시간을 잘 활용해 서로의 상황을 꾸준히 공유하여 계획에 큰 차질없이 프로젝트가 진행될 수 있었던 것 같습니다. + - 백엔드 팀원들간의 코드 리뷰를 통해 서로의 코드를 공유하고 의견을 나누며 많이 배울 수 있는 시간이었습니다. + +- **프로젝트를 하면서 잘했던 점** + - 매일 10시, 17시 데일리 스크럼 시간을 잘 활용해 서로의 상황을 꾸준히 공유하여 계획에 큰 차질없이 프로젝트가 진행될 수 있었던 것 같습니다. + +- **프로젝트를 진행하면서 힘들었던 점** + - 프론트엔드에 대해 지식이 부족하기 때문에 프론트엔드가 설명해주는 것을 빠르게 이해하고 소통하는데 어려움을 겪었습니다. 이번 프로젝트를 통해 프론트엔드에 대해 조금 알아갈 수 있었던 것 같습니다. + +
+
+ +
+심재철 +
+ +- **배운 점** + - 프로젝트를 통해 나의 경험 중 하나로, 효과적인 협업을 위해서는 자신의 진행 상황을 솔직하게, 빠르게 전달하는 것이 상당히 중요하다는 것을 깨달았습니다. 투명하게 소통함으로써 팀 전체가 현재 상황을 정확히 파악하고, 문제 발생 시 신속한 대응이 가능하다는 것을 인지했습니다. + +- **프로젝트를 하면서 잘했던 점** + - 프로젝트 진행 중에는 효과적인 협업을 실현하기 위해 서로의 진행 상황을 체계적으로 공유하면서 개발을 진행했습니다. + - 팀 원 개개인이 빈 시간이 발생할 때 주도적으로 추가 기능이나 프로젝트에 필요한 작업들을 수행하고자 해서 프로젝트 일정에 맞출 수 있었습니다. + +- **프로젝트를 진행하면서 힘들었던 점** + - 프로젝트에서 가장 어려웠던 부분은 팀 구성원이 3명이어서 요구사항에 따른 API 개발을 프로젝트 일정에 맞추는데 빠듯함이 있었습니다. + +
+
+ +## 7. API 문서 ※ Spring REST Docs로 문서화했습니다. > `index` -> ![](src/main/resources/image/index-docs.png) +> ![](src/main/resources/image/docs/index-docs.png) > > `Member API Docs` -> ![](src/main/resources/image/member-docs.png) +> ![](src/main/resources/image/docs/member-docs.png) > > `Product API Docs` -> ![](src/main/resources/image/product-docs.png) +> ![](src/main/resources/image/docs/product-docs.png) +> +> `Room API Docs` +> ![](src/main/resources/image/docs/room-docs.png) > > `Cart API Docs` -> ![](src/main/resources/image/cart-docs.png) +> ![](src/main/resources/image/docs/cart-docs.png) > > `Reservation API Docs` -> ![](src/main/resources/image/reservation-docs.png) +> ![](src/main/resources/image/docs/reservation-docs.png) > > `Reservation Product API Docs` -> ![](src/main/resources/image/reservation-product-docs.png) +> ![](src/main/resources/image/docs/reservation-product-docs.png) > > `Star API Docs` -> ![](src/main/resources/image/star-docs.png) -> \ No newline at end of file +> ![](src/main/resources/image/docs/star-docs.png) +> +> `Favorite API Docs` +> ![](src/main/resources/image/docs/favorite-docs.png) diff --git a/src/docs/asciidoc/cart/cart-api.adoc b/src/docs/asciidoc/cart/cart-api.adoc index db9c0f1a..dbac0573 100644 --- a/src/docs/asciidoc/cart/cart-api.adoc +++ b/src/docs/asciidoc/cart/cart-api.adoc @@ -2,7 +2,7 @@ ifndef::snippets[] :snippets: build/generated-snippets endif::[] -= Member REST API Docs += Cart REST API Docs :doctype: book :icons: font :source-highlighter: highlightjs diff --git a/src/docs/asciidoc/favorite/favorite-api.adoc b/src/docs/asciidoc/favorite/favorite-api.adoc new file mode 100644 index 00000000..fedccd13 --- /dev/null +++ b/src/docs/asciidoc/favorite/favorite-api.adoc @@ -0,0 +1,52 @@ +ifndef::snippets[] +:snippets: build/generated-snippets +endif::[] + += Favorite REST API Docs +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 2 + +[[Register]] +== 즐겨찾기 등록 + +즐겨찾기 등록 API 입니다. + +=== HttpRequest + +include::{snippets}/favorite-rest-controller-docs-test/register/http-request.adoc[] + +=== HttpResponse + +include::{snippets}/favorite-rest-controller-docs-test/register/http-response.adoc[] +include::{snippets}/favorite-rest-controller-docs-test/register/response-fields.adoc[] + +[[Get-Favorites]] +== 즐겨찾기 목록 조회 + +즐겨찾기 목록 조회 API 입니다. + +=== HttpRequest + +include::{snippets}/favorite-rest-controller-docs-test/get-favorites/http-request.adoc[] + +=== HttpResponse + +include::{snippets}/favorite-rest-controller-docs-test/get-favorites/http-response.adoc[] +include::{snippets}/favorite-rest-controller-docs-test/get-favorites/response-fields.adoc[] + +[[Cancel]] +== 즐겨찾기 취소 + +즐겨찾기 취소 API 입니다. + +=== HttpRequest + +include::{snippets}/favorite-rest-controller-docs-test/cancel/http-request.adoc[] + +=== HttpResponse + +include::{snippets}/favorite-rest-controller-docs-test/cancel/http-response.adoc[] +include::{snippets}/favorite-rest-controller-docs-test/cancel/response-fields.adoc[] diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 19edb242..5a6d322d 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -11,10 +11,20 @@ === link:member/member-api.html[회원 API, window=blank] -=== link:product/product-api.html[상품 API, window=blank] +=== link:product/product-api.html[숙소 API, window=blank] + +=== link:room/room-api.html[객실 API, window=blank] === link:cart/cart-api.html[장바구니 API, window=blank] +=== link:reservationproduct/reservation-product-api.html[예약 상품 API, window=blank] + +=== link:reservation/reservation-api.html[예약 API, window=blank] + +=== link:star/star-api.html[별점 API, window=blank] + +=== link:favorite/favorite-api.html[즐겨찾기 API, window=blank] + == API Common Response diff --git a/src/docs/asciidoc/product/product-api.adoc b/src/docs/asciidoc/product/product-api.adoc index fad29ae1..f2e87703 100644 --- a/src/docs/asciidoc/product/product-api.adoc +++ b/src/docs/asciidoc/product/product-api.adoc @@ -2,7 +2,7 @@ ifndef::snippets[] :snippets: build/generated-snippets endif::[] -= Member REST API Docs += Product REST API Docs :doctype: book :icons: font :source-highlighter: highlightjs diff --git a/src/docs/asciidoc/room/room-api.adoc b/src/docs/asciidoc/room/room-api.adoc new file mode 100644 index 00000000..39559c40 --- /dev/null +++ b/src/docs/asciidoc/room/room-api.adoc @@ -0,0 +1,25 @@ +ifndef::snippets[] +:snippets: build/generated-snippets +endif::[] + += Room REST API Docs +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 2 + +[[Get-Rooms-With-Product-Info]] +== 숙박 정보를 포함한 객실 정보 리스트 조회 + +숙박 정보를 포함한 객실 정보 리스트 조회 API 입니다. (주문 페이지에서 필요한 조회) + +=== HttpRequest + +include::{snippets}/room-rest-controller-docs-test/get-rooms-with-product-info/http-request.adoc[] +include::{snippets}/room-rest-controller-docs-test/get-rooms-with-product-info/query-parameters.adoc[] + +=== HttpResponse + +include::{snippets}/room-rest-controller-docs-test/get-rooms-with-product-info/http-response.adoc[] +include::{snippets}/room-rest-controller-docs-test/get-rooms-with-product-info/response-fields.adoc[] diff --git a/src/main/java/com/fc/shimpyo_be/domain/cart/controller/CartRestController.java b/src/main/java/com/fc/shimpyo_be/domain/cart/controller/CartRestController.java index 7e9f66da..23be1ce3 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/cart/controller/CartRestController.java +++ b/src/main/java/com/fc/shimpyo_be/domain/cart/controller/CartRestController.java @@ -13,6 +13,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -25,6 +26,7 @@ @RestController @RequestMapping("/api/carts") @RequiredArgsConstructor +@Transactional(readOnly = true) public class CartRestController { private final CartService cartService; @@ -36,10 +38,9 @@ public ResponseEntity>> getCarts() { } @PostMapping + @Transactional public ResponseEntity> addCart( @Valid @RequestBody CartCreateRequest cartCreateRequest) { - log.debug("startDate: {}, endDate: {}, price: {}", cartCreateRequest.startDate(), - cartCreateRequest.endDate(), cartCreateRequest.price()); if (DateTimeUtil.isNotValidDate(DateTimeUtil.toLocalDate(cartCreateRequest.startDate()), DateTimeUtil.toLocalDate(cartCreateRequest.endDate()))) { throw new InvalidDateException(); @@ -50,9 +51,9 @@ public ResponseEntity> addCart( } @DeleteMapping("/{cartId}") + @Transactional public ResponseEntity> deleteCart( @PathVariable("cartId") Long cartId) { - log.debug("cartId: {}", cartId); return ResponseEntity.ok().body( ResponseDto.res(HttpStatus.OK, cartService.deleteCart(cartId), "장바구니를 성공적으로 삭제했습니다.")); } diff --git a/src/main/java/com/fc/shimpyo_be/domain/cart/dto/request/CartCreateRequest.java b/src/main/java/com/fc/shimpyo_be/domain/cart/dto/request/CartCreateRequest.java index 7d775828..03188608 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/cart/dto/request/CartCreateRequest.java +++ b/src/main/java/com/fc/shimpyo_be/domain/cart/dto/request/CartCreateRequest.java @@ -5,7 +5,7 @@ import jakarta.validation.constraints.Pattern; import lombok.Builder; -public record CartCreateRequest(@NotNull Long roomId, +public record CartCreateRequest(@NotNull Long roomCode, @Pattern(regexp = DateTimeUtil.LOCAL_DATE_REGEX_PATTERN, message = "잘못된 시간 형식입니다. (올바른 예시: 2023-10-25)") String startDate, @Pattern(regexp = DateTimeUtil.LOCAL_DATE_REGEX_PATTERN, message = "잘못된 시간 형식입니다. (올바른 예시: 2023-10-25)") String endDate, @@ -13,8 +13,8 @@ public record CartCreateRequest(@NotNull Long roomId, Long price) { @Builder - public CartCreateRequest(Long roomId, String startDate, String endDate, Long price) { - this.roomId = roomId; + public CartCreateRequest(Long roomCode, String startDate, String endDate, Long price) { + this.roomCode = roomCode; this.startDate = startDate; this.endDate = endDate; this.price = price; diff --git a/src/main/java/com/fc/shimpyo_be/domain/cart/dto/response/CartDeleteResponse.java b/src/main/java/com/fc/shimpyo_be/domain/cart/dto/response/CartDeleteResponse.java index ebfd9553..1b65f396 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/cart/dto/response/CartDeleteResponse.java +++ b/src/main/java/com/fc/shimpyo_be/domain/cart/dto/response/CartDeleteResponse.java @@ -2,12 +2,12 @@ import lombok.Builder; -public record CartDeleteResponse(Long cartId, Long roomId, String startDate, String endDate) { +public record CartDeleteResponse(Long cartId, Long roomCode, String startDate, String endDate) { @Builder - public CartDeleteResponse(Long cartId, Long roomId, String startDate, String endDate) { + public CartDeleteResponse(Long cartId, Long roomCode, String startDate, String endDate) { this.cartId = cartId; - this.roomId = roomId; + this.roomCode = roomCode; this.startDate = startDate; this.endDate = endDate; } diff --git a/src/main/java/com/fc/shimpyo_be/domain/cart/dto/response/CartResponse.java b/src/main/java/com/fc/shimpyo_be/domain/cart/dto/response/CartResponse.java index dc519468..609df20b 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/cart/dto/response/CartResponse.java +++ b/src/main/java/com/fc/shimpyo_be/domain/cart/dto/response/CartResponse.java @@ -11,7 +11,7 @@ public class CartResponse { private final Long productId; private final String productName; private final String image; - private final Long roomId; + private final Long roomCode; private final String roomName; private final Long price; private final String description; @@ -21,21 +21,17 @@ public class CartResponse { private final String endDate; private final String checkIn; private final String checkOut; - private Boolean reserved = false; - - public void setReserved(){ - reserved = true; - } @Builder - public CartResponse(Long cartId, Long productId, String productName, String image, Long roomId, + private CartResponse(Long cartId, Long productId, String productName, String image, + Long roomCode, String roomName, Long price, String description, Long standard, Long capacity, - String startDate, String endDate, String checkIn, String checkOut, Boolean reserved) { + String startDate, String endDate, String checkIn, String checkOut) { this.cartId = cartId; this.productId = productId; this.productName = productName; this.image = image; - this.roomId = roomId; + this.roomCode = roomCode; this.roomName = roomName; this.price = price; this.description = description; diff --git a/src/main/java/com/fc/shimpyo_be/domain/cart/entity/Cart.java b/src/main/java/com/fc/shimpyo_be/domain/cart/entity/Cart.java index 0b1d272a..9b3b4a45 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/cart/entity/Cart.java +++ b/src/main/java/com/fc/shimpyo_be/domain/cart/entity/Cart.java @@ -1,7 +1,6 @@ package com.fc.shimpyo_be.domain.cart.entity; import com.fc.shimpyo_be.domain.member.entity.Member; -import com.fc.shimpyo_be.domain.room.entity.Room; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -15,6 +14,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -23,28 +23,29 @@ public class Cart { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment(value = "장바구니 식별자") private Long id; - + @Comment(value = "회원 식별자") @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id", nullable = false) private Member member; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id", nullable = false) - private Room room; + @Comment(value = "객실 코드") + @Column(nullable = false) + private Long roomCode; + @Comment(value = "총 이용 금액") @Column(nullable = false) private Long price; - + @Comment(value = "숙박 시작일") @Column(nullable = false) private LocalDate startDate; - + @Comment(value = "숙박 마지막일") @Column(nullable = false) private LocalDate endDate; @Builder - public Cart(Room room, Member member, Long price, LocalDate startDate, LocalDate endDate) { - this.room = room; + public Cart(Long roomCode, Member member, Long price, LocalDate startDate, LocalDate endDate) { + this.roomCode = roomCode; this.member = member; this.price = price; this.startDate = startDate; diff --git a/src/main/java/com/fc/shimpyo_be/domain/cart/repository/CartCustomRepository.java b/src/main/java/com/fc/shimpyo_be/domain/cart/repository/CartCustomRepository.java new file mode 100644 index 00000000..71a25423 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/cart/repository/CartCustomRepository.java @@ -0,0 +1,9 @@ +package com.fc.shimpyo_be.domain.cart.repository; + +import com.fc.shimpyo_be.domain.cart.dto.request.CartCreateRequest; +import java.time.LocalDate; + +public interface CartCustomRepository { + Long countByRoomCodeAndMemberIdContainsDate (CartCreateRequest cartCreateRequest, Long memberId); +} + diff --git a/src/main/java/com/fc/shimpyo_be/domain/cart/repository/CartCustomRepositoryImpl.java b/src/main/java/com/fc/shimpyo_be/domain/cart/repository/CartCustomRepositoryImpl.java new file mode 100644 index 00000000..dd4adcd2 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/cart/repository/CartCustomRepositoryImpl.java @@ -0,0 +1,52 @@ +package com.fc.shimpyo_be.domain.cart.repository; + +import static com.fc.shimpyo_be.domain.cart.entity.QCart.cart; + +import com.fc.shimpyo_be.domain.cart.dto.request.CartCreateRequest; +import com.fc.shimpyo_be.global.util.DateTimeUtil; +import com.querydsl.core.QueryException; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.DateTemplate; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import org.springframework.stereotype.Repository; + +@Repository +public class CartCustomRepositoryImpl implements CartCustomRepository { + + private final JPAQueryFactory queryFactory; + + CartCustomRepositoryImpl(JPAQueryFactory jpaQueryFactory) { + this.queryFactory = jpaQueryFactory; + } + + @Override + public Long countByRoomCodeAndMemberIdContainsDate(CartCreateRequest cartCreateRequest, + Long memberId) { + + LocalDate startDate = DateTimeUtil.toLocalDate(cartCreateRequest.startDate()); + LocalDate endDate = DateTimeUtil.toLocalDate(cartCreateRequest.endDate()); + + return queryFactory + .selectFrom(cart) + .leftJoin(cart.member) + .where(buildSearchConditions(cartCreateRequest.roomCode(), memberId, + startDate, endDate)).fetchCount(); + } + + private BooleanExpression buildSearchConditions(Long roomCode, Long memberId, + LocalDate startDate, LocalDate endDate) { + List expressions = new ArrayList<>(); + + if (roomCode == null || memberId == null || startDate == null || endDate == null) { + throw new QueryException("잘못된 쿼리 입니다."); + } + + expressions.add(cart.member.id.eq(memberId).and(cart.roomCode.eq(roomCode)) + .and(cart.startDate.before(endDate).and(cart.endDate.after(startDate)))); + + return expressions.stream().reduce(BooleanExpression::and).orElse(null); + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/cart/repository/CartRepository.java b/src/main/java/com/fc/shimpyo_be/domain/cart/repository/CartRepository.java index f717a6b9..81804cc8 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/cart/repository/CartRepository.java +++ b/src/main/java/com/fc/shimpyo_be/domain/cart/repository/CartRepository.java @@ -2,6 +2,7 @@ import com.fc.shimpyo_be.domain.cart.entity.Cart; +import java.time.LocalDate; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -9,4 +10,5 @@ public interface CartRepository extends JpaRepository { Optional> findByMemberId(Long memberId); + } diff --git a/src/main/java/com/fc/shimpyo_be/domain/cart/service/CartService.java b/src/main/java/com/fc/shimpyo_be/domain/cart/service/CartService.java index 965815ca..a30361b9 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/cart/service/CartService.java +++ b/src/main/java/com/fc/shimpyo_be/domain/cart/service/CartService.java @@ -6,12 +6,12 @@ import com.fc.shimpyo_be.domain.cart.entity.Cart; import com.fc.shimpyo_be.domain.cart.exception.CartNotDeleteException; import com.fc.shimpyo_be.domain.cart.exception.CartNotFoundException; +import com.fc.shimpyo_be.domain.cart.repository.CartCustomRepositoryImpl; import com.fc.shimpyo_be.domain.cart.repository.CartRepository; import com.fc.shimpyo_be.domain.cart.util.CartMapper; import com.fc.shimpyo_be.domain.member.entity.Member; import com.fc.shimpyo_be.domain.member.exception.MemberNotFoundException; import com.fc.shimpyo_be.domain.member.repository.MemberRepository; -import com.fc.shimpyo_be.domain.product.exception.RoomNotFoundException; import com.fc.shimpyo_be.domain.product.exception.RoomNotReserveException; import com.fc.shimpyo_be.domain.product.service.ProductService; import com.fc.shimpyo_be.domain.room.entity.Room; @@ -19,6 +19,7 @@ import com.fc.shimpyo_be.global.util.SecurityUtil; import jakarta.validation.Valid; import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -38,15 +39,13 @@ public class CartService { private final ProductService productService; + private final CartCustomRepositoryImpl cartCustomRepository; + public List getCarts() { - List cartResponses= cartRepository.findByMemberId(securityUtil.getCurrentMemberId()).orElseThrow() - .stream().map(CartMapper::toCartResponse).toList(); - cartResponses.stream().filter( - cartResponse -> !productService.isAvailableForReservation(cartResponse.getRoomId(), - cartResponse.getStartDate(), cartResponse.getEndDate())).forEach( - CartResponse::setReserved); - - return cartResponses; + List carts = cartRepository.findByMemberId( + securityUtil.getCurrentMemberId()).orElseThrow(); + + return carts.stream().map(this::getCartResponse).toList(); } @Transactional @@ -55,19 +54,24 @@ public CartResponse addCart(@Valid @RequestBody CartCreateRequest cartCreateRequ Member member = memberRepository.findById(securityUtil.getCurrentMemberId()) .orElseThrow(MemberNotFoundException::new); - if (!productService.isAvailableForReservation(cartCreateRequest.roomId(), - cartCreateRequest.startDate(), cartCreateRequest.endDate())) { + Long countAvailableForReservation = productService.countAvailableForReservationUsingRoomCode( + cartCreateRequest.roomCode(), + cartCreateRequest.startDate(), + cartCreateRequest.endDate()); + + if (countAvailableForReservation <= 0 + || cartCustomRepository.countByRoomCodeAndMemberIdContainsDate( + cartCreateRequest, member.getId()) + 1 + > countAvailableForReservation) { throw new RoomNotReserveException(); } - Room room = roomRepository.findById(cartCreateRequest.roomId()) - .orElseThrow(RoomNotFoundException::new); - Cart createdCart = cartRepository.save(CartMapper.toCart(cartCreateRequest, room, member)); - return CartMapper.toCartResponse(createdCart); + Cart createdCart = cartRepository.save(CartMapper.toCart(cartCreateRequest, member)); + return getCartResponse(createdCart); } @Transactional - public CartDeleteResponse deleteCart(Long cartId) { + public CartDeleteResponse deleteCart(final Long cartId) { Cart cart = cartRepository.findById(cartId).orElseThrow(CartNotFoundException::new); if (!cart.getMember().getId().equals(securityUtil.getCurrentMemberId())) { @@ -78,5 +82,24 @@ public CartDeleteResponse deleteCart(Long cartId) { return CartMapper.toCartDeleteResponse(cart); } + @Transactional + public CartDeleteResponse deleteCart(final Long memberId, final Long cartId) { + Cart cart = cartRepository.findById(cartId).orElseThrow(CartNotFoundException::new); + + if (!cart.getMember().getId().equals(memberId)) { + throw new CartNotDeleteException(); + } + cartRepository.deleteById(cart.getId()); + + return CartMapper.toCartDeleteResponse(cart); + } + + private CartResponse getCartResponse(final Cart cart) { + List rooms = Optional.of(roomRepository.findByCode(cart.getRoomCode())) + .orElseThrow(); + + return CartMapper.toCartResponse(cart, rooms.get(0)); + } + } diff --git a/src/main/java/com/fc/shimpyo_be/domain/cart/util/CartMapper.java b/src/main/java/com/fc/shimpyo_be/domain/cart/util/CartMapper.java index 4a815e46..89658b59 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/cart/util/CartMapper.java +++ b/src/main/java/com/fc/shimpyo_be/domain/cart/util/CartMapper.java @@ -11,12 +11,11 @@ public class CartMapper { - public static CartResponse toCartResponse(Cart cart) { - Room room = cart.getRoom(); + public static CartResponse toCartResponse(Cart cart, Room room) { Product product = room.getProduct(); return CartResponse.builder().cartId(cart.getId()).productId(product.getId()) - .productName(product.getName()).image(product.getThumbnail()).roomId(room.getId()) + .productName(product.getName()).image(product.getThumbnail()).roomCode(room.getCode()) .roomName(room.getName()).price(cart.getPrice()).description(room.getDescription()) .standard((long) room.getStandard()).capacity((long) room.getCapacity()) .startDate(DateTimeUtil.toString(cart.getStartDate())) @@ -25,14 +24,14 @@ public static CartResponse toCartResponse(Cart cart) { } public static CartDeleteResponse toCartDeleteResponse(Cart cart) { - return CartDeleteResponse.builder().cartId(cart.getId()).roomId(cart.getRoom().getId()) + return CartDeleteResponse.builder().cartId(cart.getId()).roomCode(cart.getRoomCode()) .startDate(DateTimeUtil.toString(cart.getStartDate())) .endDate(DateTimeUtil.toString(cart.getEndDate())).build(); } - public static Cart toCart(CartCreateRequest cartCreateRequest, Room room, Member member) { + public static Cart toCart(CartCreateRequest cartCreateRequest, Member member) { return Cart.builder() - .room(room) + .roomCode(cartCreateRequest.roomCode()) .member(member) .price(cartCreateRequest.price()) .startDate(DateTimeUtil.toLocalDate(cartCreateRequest.startDate())) diff --git a/src/main/java/com/fc/shimpyo_be/domain/favorite/controller/FavoriteRestController.java b/src/main/java/com/fc/shimpyo_be/domain/favorite/controller/FavoriteRestController.java new file mode 100644 index 00000000..9980ffab --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/favorite/controller/FavoriteRestController.java @@ -0,0 +1,53 @@ +package com.fc.shimpyo_be.domain.favorite.controller; + +import com.fc.shimpyo_be.domain.favorite.dto.FavoriteResponseDto; +import com.fc.shimpyo_be.domain.favorite.dto.FavoritesResponseDto; +import com.fc.shimpyo_be.domain.favorite.entity.Favorite; +import com.fc.shimpyo_be.domain.favorite.service.FavoriteService; +import com.fc.shimpyo_be.domain.product.util.model.PageableConstraint; +import com.fc.shimpyo_be.global.common.ResponseDto; +import com.fc.shimpyo_be.global.util.SecurityUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/favorites") +public class FavoriteRestController { + + private final FavoriteService favoriteService; + private final SecurityUtil securityUtil; + private static final int DEFAULT_SIZE = 10; + private static final int DEFAULT_PAGE = 0; + + @PostMapping("/{productId}") + public ResponseEntity> register(@PathVariable long productId) { + return ResponseEntity.status(HttpStatus.CREATED).body(ResponseDto.res(HttpStatus.CREATED, + favoriteService.register(securityUtil.getCurrentMemberId(), productId), + "성공적으로 즐겨찾기를 등록했습니다.")); + } + + @GetMapping + public ResponseEntity> getFavorites( + @PageableConstraint(Favorite.class) @PageableDefault(size = DEFAULT_SIZE, page = DEFAULT_PAGE) Pageable pageable) { + return ResponseEntity.status(HttpStatus.OK) + .body(ResponseDto.res(HttpStatus.OK, favoriteService.getFavorites( + securityUtil.getCurrentMemberId(), pageable), "성공적으로 즐겨찾기 목록을 조회했습니다.")); + } + + @DeleteMapping("/{productId}") + public ResponseEntity> cancel(@PathVariable long productId) { + return ResponseEntity.status(HttpStatus.OK).body(ResponseDto.res(HttpStatus.OK, + favoriteService.delete(securityUtil.getCurrentMemberId(), productId), + "성공적으로 즐겨찾기를 취소했습니다.")); + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/favorite/dto/FavoriteResponseDto.java b/src/main/java/com/fc/shimpyo_be/domain/favorite/dto/FavoriteResponseDto.java new file mode 100644 index 00000000..6594df31 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/favorite/dto/FavoriteResponseDto.java @@ -0,0 +1,31 @@ +package com.fc.shimpyo_be.domain.favorite.dto; + +import com.fc.shimpyo_be.domain.favorite.entity.Favorite; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class FavoriteResponseDto { + + private Long favoriteId; + private Long memberId; + private Long productId; + + @Builder + private FavoriteResponseDto(Long favoriteId, Long memberId, Long productId) { + this.favoriteId = favoriteId; + this.memberId = memberId; + this.productId = productId; + } + + public static FavoriteResponseDto of(Favorite favorite) { + return FavoriteResponseDto.builder() + .favoriteId(favorite.getId()) + .memberId(favorite.getMember().getId()) + .productId(favorite.getProduct().getId()) + .build(); + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/favorite/dto/FavoritesResponseDto.java b/src/main/java/com/fc/shimpyo_be/domain/favorite/dto/FavoritesResponseDto.java new file mode 100644 index 00000000..9c45db18 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/favorite/dto/FavoritesResponseDto.java @@ -0,0 +1,22 @@ +package com.fc.shimpyo_be.domain.favorite.dto; + +import com.fc.shimpyo_be.domain.product.dto.response.ProductResponse; +import java.util.List; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class FavoritesResponseDto { + + private int pageCount; + private List products; + + @Builder + private FavoritesResponseDto(int pageCount, List products) { + this.pageCount = pageCount; + this.products = products; + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/favorite/entity/Favorite.java b/src/main/java/com/fc/shimpyo_be/domain/favorite/entity/Favorite.java index d1d493ea..d4c10bf2 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/favorite/entity/Favorite.java +++ b/src/main/java/com/fc/shimpyo_be/domain/favorite/entity/Favorite.java @@ -30,7 +30,7 @@ public class Favorite { private Product product; @Builder - public Favorite(Long id, Member member, Product product) { + private Favorite(Long id, Member member, Product product) { this.id = id; this.member = member; this.product = product; diff --git a/src/main/java/com/fc/shimpyo_be/domain/favorite/exception/FavoriteAlreadyRegisterException.java b/src/main/java/com/fc/shimpyo_be/domain/favorite/exception/FavoriteAlreadyRegisterException.java new file mode 100644 index 00000000..5399ec4c --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/favorite/exception/FavoriteAlreadyRegisterException.java @@ -0,0 +1,11 @@ +package com.fc.shimpyo_be.domain.favorite.exception; + +import com.fc.shimpyo_be.global.exception.ApplicationException; +import com.fc.shimpyo_be.global.exception.ErrorCode; + +public class FavoriteAlreadyRegisterException extends ApplicationException { + + public FavoriteAlreadyRegisterException() { + super(ErrorCode.FAVORITE_ALREADY_REGISTER); + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/favorite/exception/FavoriteNotFoundException.java b/src/main/java/com/fc/shimpyo_be/domain/favorite/exception/FavoriteNotFoundException.java new file mode 100644 index 00000000..886f954a --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/favorite/exception/FavoriteNotFoundException.java @@ -0,0 +1,11 @@ +package com.fc.shimpyo_be.domain.favorite.exception; + +import com.fc.shimpyo_be.global.exception.ApplicationException; +import com.fc.shimpyo_be.global.exception.ErrorCode; + +public class FavoriteNotFoundException extends ApplicationException { + + public FavoriteNotFoundException() { + super(ErrorCode.FAVORITE_NOT_FOUND); + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/favorite/repository/FavoriteCustomRepository.java b/src/main/java/com/fc/shimpyo_be/domain/favorite/repository/FavoriteCustomRepository.java new file mode 100644 index 00000000..c77fec14 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/favorite/repository/FavoriteCustomRepository.java @@ -0,0 +1,10 @@ +package com.fc.shimpyo_be.domain.favorite.repository; + +import com.fc.shimpyo_be.domain.favorite.entity.Favorite; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface FavoriteCustomRepository { + + Page findAllByMemberId(long memberId, Pageable pageable); +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/favorite/repository/FavoriteCustomRepositoryImpl.java b/src/main/java/com/fc/shimpyo_be/domain/favorite/repository/FavoriteCustomRepositoryImpl.java new file mode 100644 index 00000000..766b6087 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/favorite/repository/FavoriteCustomRepositoryImpl.java @@ -0,0 +1,55 @@ +package com.fc.shimpyo_be.domain.favorite.repository; + +import static com.fc.shimpyo_be.domain.favorite.entity.QFavorite.favorite; +import static com.fc.shimpyo_be.domain.member.entity.QMember.member; +import static com.fc.shimpyo_be.domain.product.entity.QProduct.product; + +import com.fc.shimpyo_be.domain.favorite.entity.Favorite; +import com.fc.shimpyo_be.global.util.QueryDslUtil; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.LinkedList; +import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Repository; + +@Repository +public class FavoriteCustomRepositoryImpl implements FavoriteCustomRepository { + + private final JPAQueryFactory queryFactory; + + FavoriteCustomRepositoryImpl(JPAQueryFactory jpaQueryFactory) { + this.queryFactory = jpaQueryFactory; + } + + @Override + public Page findAllByMemberId(long memberId, Pageable pageable) { + JPAQuery query = queryFactory + .selectDistinct(favorite) + .from(favorite) + .leftJoin(favorite.member, member) + .leftJoin(favorite.product, product) + .where(member.id.eq(memberId)) + .offset(pageable.getOffset()) + .orderBy(getAllOrderSpecifiers(pageable).toArray(OrderSpecifier[]::new)) + .limit(pageable.getPageSize()); + JPAQuery countQuery = queryFactory + .selectDistinct(favorite) + .from(favorite) + .leftJoin(favorite.member, member) + .leftJoin(favorite.product, product) + .where(member.id.eq(memberId)); + List content = query.fetch(); + return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetch().size()); + } + + private List> getAllOrderSpecifiers(Pageable pageable) { + List> ORDERS = new LinkedList<>(); + ORDERS.add(QueryDslUtil.getSortedColumn(Order.DESC, favorite, "id")); + return ORDERS; + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/favorite/repository/FavoriteRepository.java b/src/main/java/com/fc/shimpyo_be/domain/favorite/repository/FavoriteRepository.java index a4cbaa1e..9b06b4bb 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/favorite/repository/FavoriteRepository.java +++ b/src/main/java/com/fc/shimpyo_be/domain/favorite/repository/FavoriteRepository.java @@ -1,8 +1,13 @@ package com.fc.shimpyo_be.domain.favorite.repository; import com.fc.shimpyo_be.domain.favorite.entity.Favorite; +import com.fc.shimpyo_be.domain.member.entity.Member; +import com.fc.shimpyo_be.domain.product.entity.Product; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -public interface FavoriteRepository extends JpaRepository { +public interface FavoriteRepository extends JpaRepository, + FavoriteCustomRepository { + Optional findByMemberAndProduct(Member member, Product product); } diff --git a/src/main/java/com/fc/shimpyo_be/domain/favorite/service/FavoriteService.java b/src/main/java/com/fc/shimpyo_be/domain/favorite/service/FavoriteService.java new file mode 100644 index 00000000..cd4986c8 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/favorite/service/FavoriteService.java @@ -0,0 +1,69 @@ +package com.fc.shimpyo_be.domain.favorite.service; + +import com.fc.shimpyo_be.domain.favorite.dto.FavoriteResponseDto; +import com.fc.shimpyo_be.domain.favorite.dto.FavoritesResponseDto; +import com.fc.shimpyo_be.domain.favorite.entity.Favorite; +import com.fc.shimpyo_be.domain.favorite.exception.FavoriteAlreadyRegisterException; +import com.fc.shimpyo_be.domain.favorite.exception.FavoriteNotFoundException; +import com.fc.shimpyo_be.domain.favorite.repository.FavoriteRepository; +import com.fc.shimpyo_be.domain.member.entity.Member; +import com.fc.shimpyo_be.domain.member.service.MemberService; +import com.fc.shimpyo_be.domain.product.dto.response.ProductResponse; +import com.fc.shimpyo_be.domain.product.entity.Product; +import com.fc.shimpyo_be.domain.product.exception.ProductNotFoundException; +import com.fc.shimpyo_be.domain.product.repository.ProductRepository; +import com.fc.shimpyo_be.domain.product.util.ProductMapper; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class FavoriteService { + + private final FavoriteRepository favoriteRepository; + private final MemberService memberService; + private final ProductRepository productRepository; + + public FavoriteResponseDto register(long memberId, long productId) { + Member member = memberService.getMemberById(memberId); + Product product = productRepository.findById(productId).orElseThrow( + ProductNotFoundException::new); + Optional favorite = favoriteRepository.findByMemberAndProduct(member, product); + if (favorite.isPresent()) { + throw new FavoriteAlreadyRegisterException(); + } + return FavoriteResponseDto.of(favoriteRepository.save(Favorite.builder() + .member(member) + .product(product) + .build())); + } + + public FavoritesResponseDto getFavorites(long memberId, Pageable pageable) { + List productResponses = new ArrayList<>(); + Member member = memberService.getMemberById(memberId); + Page favorites = favoriteRepository.findAllByMemberId(member.getId(), pageable); + for (Favorite favorite : favorites) { + productResponses.add(ProductMapper.toProductResponse(favorite.getProduct(),true)); + } + return FavoritesResponseDto.builder() + .pageCount(favorites.getTotalPages()) + .products(productResponses) + .build(); + } + + public FavoriteResponseDto delete(long memberId, long productId) { + Member member = memberService.getMemberById(memberId); + Product product = productRepository.findById(productId).orElseThrow( + ProductNotFoundException::new); + Favorite favorite = favoriteRepository.findByMemberAndProduct(member, product) + .orElseThrow(FavoriteNotFoundException::new); + FavoriteResponseDto favoriteResponseDto = FavoriteResponseDto.of(favorite); + favoriteRepository.delete(favorite); + return favoriteResponseDto; + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/member/controller/AuthRestController.java b/src/main/java/com/fc/shimpyo_be/domain/member/controller/AuthRestController.java index 07744978..d62db927 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/member/controller/AuthRestController.java +++ b/src/main/java/com/fc/shimpyo_be/domain/member/controller/AuthRestController.java @@ -9,7 +9,6 @@ import com.fc.shimpyo_be.global.common.ResponseDto; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -17,7 +16,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Slf4j @RestController @RequiredArgsConstructor @RequestMapping("/api/auth") @@ -28,7 +26,6 @@ public class AuthRestController { @PostMapping("/signup") public ResponseEntity> signUp( @Valid @RequestBody SignUpRequestDto signUpRequestDto) { - log.debug("email: {}, name: {}", signUpRequestDto.getEmail(), signUpRequestDto.getName()); return ResponseEntity.status(HttpStatus.CREATED).body( ResponseDto.res(HttpStatus.CREATED, authService.signUp(signUpRequestDto), "성공적으로 회원가입 했습니다.")); @@ -37,7 +34,6 @@ public ResponseEntity> signUp( @PostMapping("/signin") public ResponseEntity> signIn( @Valid @RequestBody SignInRequestDto signInRequestDto) { - log.debug("email: {}", signInRequestDto.getEmail()); return ResponseEntity.status(HttpStatus.OK).body( ResponseDto.res(HttpStatus.OK, authService.signIn(signInRequestDto), "성공적으로 로그인 했습니다.")); @@ -46,8 +42,6 @@ public ResponseEntity> signIn( @PostMapping("/refresh") public ResponseEntity> refresh( @Valid @RequestBody RefreshRequestDto refreshRequestDto) { - log.debug("accessToken: {}, refreshToken: {}", refreshRequestDto.getAccessToken(), - refreshRequestDto.getRefreshToken()); return ResponseEntity.status(HttpStatus.OK).body( ResponseDto.res(HttpStatus.OK, authService.refresh(refreshRequestDto), "성공적으로 토큰을 재발급 했습니다.")); diff --git a/src/main/java/com/fc/shimpyo_be/domain/member/controller/MemberRestController.java b/src/main/java/com/fc/shimpyo_be/domain/member/controller/MemberRestController.java index 5393e4eb..629d2e16 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/member/controller/MemberRestController.java +++ b/src/main/java/com/fc/shimpyo_be/domain/member/controller/MemberRestController.java @@ -7,7 +7,6 @@ import com.fc.shimpyo_be.global.common.ResponseDto; import com.fc.shimpyo_be.global.util.SecurityUtil; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -17,7 +16,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Slf4j @RestController @RequiredArgsConstructor @RequestMapping("/api/members") @@ -28,7 +26,6 @@ public class MemberRestController { @GetMapping public ResponseEntity> getMember() { - log.debug("memberId: {}", securityUtil.getCurrentMemberId()); return ResponseEntity.status(HttpStatus.OK).body( ResponseDto.res(HttpStatus.OK, memberService.getMember(), "성공적으로 회원 정보를 조회했습니다.")); @@ -37,7 +34,6 @@ public ResponseEntity> getMember() { @PatchMapping public ResponseEntity> updateMember(@RequestBody UpdateMemberRequestDto updateMemberRequestDto) { - log.debug("memberId: {}", securityUtil.getCurrentMemberId()); return ResponseEntity.status(HttpStatus.OK).body( ResponseDto.res(HttpStatus.OK, memberService.updateMember(updateMemberRequestDto), "성공적으로 회원 정보를 수정했습니다.")); @@ -46,7 +42,6 @@ public ResponseEntity> updateMember(@RequestBody @PostMapping public ResponseEntity> checkPassword( @RequestBody CheckPasswordRequestDto checkPasswordRequestDto) { - log.debug("memberId: {}", securityUtil.getCurrentMemberId()); memberService.checkPassword(checkPasswordRequestDto); return ResponseEntity.status(HttpStatus.OK).body( ResponseDto.res(HttpStatus.OK, "비밀번호가 일치합니다.")); diff --git a/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/CheckPasswordRequestDto.java b/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/CheckPasswordRequestDto.java index af1324bc..0bfafe93 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/CheckPasswordRequestDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/CheckPasswordRequestDto.java @@ -1,19 +1,20 @@ package com.fc.shimpyo_be.domain.member.dto.request; import jakarta.validation.constraints.NotBlank; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class CheckPasswordRequestDto { @NotBlank(message = "비밀번호를 입력하세요.") private String password; @Builder - public CheckPasswordRequestDto(String password) { + private CheckPasswordRequestDto(String password) { this.password = password; } } diff --git a/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/RefreshRequestDto.java b/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/RefreshRequestDto.java index 3523d5d6..740aecb6 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/RefreshRequestDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/RefreshRequestDto.java @@ -1,12 +1,13 @@ package com.fc.shimpyo_be.domain.member.dto.request; import jakarta.validation.constraints.NotBlank; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class RefreshRequestDto { @NotBlank(message = "Access Token 을 입력하세요.") @@ -15,7 +16,7 @@ public class RefreshRequestDto { private String refreshToken; @Builder - public RefreshRequestDto(String accessToken, String refreshToken) { + private RefreshRequestDto(String accessToken, String refreshToken) { this.accessToken = accessToken; this.refreshToken = refreshToken; } diff --git a/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/SignInRequestDto.java b/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/SignInRequestDto.java index ea765cdf..4be9b0e6 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/SignInRequestDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/SignInRequestDto.java @@ -2,14 +2,14 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class SignInRequestDto { @NotBlank(message = "이메일을 입력하세요.") @@ -19,12 +19,12 @@ public class SignInRequestDto { private String password; @Builder - public SignInRequestDto(String email, String password) { + private SignInRequestDto(String email, String password) { this.email = email; this.password = password; } - public UsernamePasswordAuthenticationToken toAuthentication(){ + public UsernamePasswordAuthenticationToken toAuthentication() { return new UsernamePasswordAuthenticationToken(this.email, this.password); } } diff --git a/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/SignUpRequestDto.java b/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/SignUpRequestDto.java index 30dedcd0..ed0425f2 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/SignUpRequestDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/SignUpRequestDto.java @@ -5,28 +5,36 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class SignUpRequestDto { + private static final String EMAIL_REGEX = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"; + private static final int NAME_MIN = 2; + private static final int NAME_MAX = 30; + private static final String PASSWORD_REGEX = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$#^()!%*?&])[A-Za-z\\d@$!#^()%*?&]{8,30}$"; + @NotBlank(message = "이메일을 입력하세요.") - @Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", message = "이메일 형식에 맞게 입력해주세요.") + @Pattern(regexp = EMAIL_REGEX, message = "이메일 형식에 맞게 입력해주세요.") private String email; @NotBlank(message = "이름을 입력하세요.") - @Size(min = 2, max = 30, message = "이름은 최소 2자 이상 최대 30자 이내로 입력하세요.") + @Size(min = NAME_MIN, max = NAME_MAX, message = "이름은 최소 2자 이상 최대 30자 이내로 입력하세요.") private String name; @NotBlank(message = "비밀번호를 입력하세요.") + @Pattern(regexp = PASSWORD_REGEX) private String password; @NotBlank(message = "비밀번호 확인을 입력하세요.") + @Pattern(regexp = PASSWORD_REGEX) private String passwordConfirm; @Builder - public SignUpRequestDto(String email, String name, String password, String passwordConfirm) { + private SignUpRequestDto(String email, String name, String password, String passwordConfirm) { this.email = email; this.name = name; this.password = password; diff --git a/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/UpdateMemberRequestDto.java b/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/UpdateMemberRequestDto.java index 0bef7927..445f874f 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/UpdateMemberRequestDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/member/dto/request/UpdateMemberRequestDto.java @@ -1,11 +1,12 @@ package com.fc.shimpyo_be.domain.member.dto.request; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class UpdateMemberRequestDto { private String password; @@ -13,7 +14,7 @@ public class UpdateMemberRequestDto { private String photoUrl; @Builder - public UpdateMemberRequestDto(String password, String passwordConfirm, String photoUrl) { + private UpdateMemberRequestDto(String password, String passwordConfirm, String photoUrl) { this.password = password; this.passwordConfirm = passwordConfirm; this.photoUrl = photoUrl; diff --git a/src/main/java/com/fc/shimpyo_be/domain/member/dto/response/MemberResponseDto.java b/src/main/java/com/fc/shimpyo_be/domain/member/dto/response/MemberResponseDto.java index 949a10c4..aa80ac89 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/member/dto/response/MemberResponseDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/member/dto/response/MemberResponseDto.java @@ -1,12 +1,13 @@ package com.fc.shimpyo_be.domain.member.dto.response; import com.fc.shimpyo_be.domain.member.entity.Member; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class MemberResponseDto { private Long memberId; @@ -15,7 +16,7 @@ public class MemberResponseDto { private String photoUrl; @Builder - public MemberResponseDto(Long memberId, String email, String name, String photoUrl) { + private MemberResponseDto(Long memberId, String email, String name, String photoUrl) { this.memberId = memberId; this.email = email; this.name = name; diff --git a/src/main/java/com/fc/shimpyo_be/domain/member/dto/response/SignInResponseDto.java b/src/main/java/com/fc/shimpyo_be/domain/member/dto/response/SignInResponseDto.java index 89fd6040..352c70cd 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/member/dto/response/SignInResponseDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/member/dto/response/SignInResponseDto.java @@ -1,18 +1,19 @@ package com.fc.shimpyo_be.domain.member.dto.response; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class SignInResponseDto { private MemberResponseDto member; private TokenResponseDto token; @Builder - public SignInResponseDto(MemberResponseDto member, TokenResponseDto token) { + private SignInResponseDto(MemberResponseDto member, TokenResponseDto token) { this.member = member; this.token = token; } diff --git a/src/main/java/com/fc/shimpyo_be/domain/member/dto/response/TokenResponseDto.java b/src/main/java/com/fc/shimpyo_be/domain/member/dto/response/TokenResponseDto.java index 582a440b..7d5ce9f5 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/member/dto/response/TokenResponseDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/member/dto/response/TokenResponseDto.java @@ -1,11 +1,12 @@ package com.fc.shimpyo_be.domain.member.dto.response; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class TokenResponseDto { private String grantType; @@ -14,7 +15,7 @@ public class TokenResponseDto { private String refreshToken; @Builder - public TokenResponseDto(String grantType, String accessToken, long accessTokenExpiresIn, + private TokenResponseDto(String grantType, String accessToken, long accessTokenExpiresIn, String refreshToken) { this.grantType = grantType; this.accessToken = accessToken; diff --git a/src/main/java/com/fc/shimpyo_be/domain/member/entity/Member.java b/src/main/java/com/fc/shimpyo_be/domain/member/entity/Member.java index 6a621584..94917b07 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/member/entity/Member.java +++ b/src/main/java/com/fc/shimpyo_be/domain/member/entity/Member.java @@ -13,6 +13,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -21,20 +22,26 @@ public class Member extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment("회원 식별자") private Long id; @Column(unique = true, nullable = false, length = 30) + @Comment("회원 이메일") private String email; @Column(nullable = false, length = 30) + @Comment("회원 이름") private String name; @Column(nullable = false) + @Comment("암호화된 비밀번호") private String password; @Column(nullable = false, columnDefinition = "TEXT") + @Comment("프로필 사진 URL") private String photoUrl; @Enumerated(EnumType.STRING) + @Comment("권한") private Authority authority; @Builder - public Member(Long id, String email, String name, String password, String photoUrl, + private Member(Long id, String email, String name, String password, String photoUrl, Authority authority) { this.id = id; this.email = email; @@ -50,7 +57,7 @@ public void update(UpdateMemberRequestDto updateMemberRequestDto) { } } - public void changePassword(String password){ + public void changePassword(String password) { this.password = password; } } diff --git a/src/main/java/com/fc/shimpyo_be/domain/member/entity/RefreshToken.java b/src/main/java/com/fc/shimpyo_be/domain/member/entity/RefreshToken.java index fd6d16fb..211d0864 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/member/entity/RefreshToken.java +++ b/src/main/java/com/fc/shimpyo_be/domain/member/entity/RefreshToken.java @@ -14,12 +14,13 @@ public class RefreshToken { @Id - @Comment("member_id") + @Comment("Refresh 토큰 식별자(회원 식별자)") private Long id; + @Comment("Refresh 토큰") private String token; @Builder - public RefreshToken(Long id, String token) { + private RefreshToken(Long id, String token) { this.id = id; this.token = token; } diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/controller/ProductRestController.java b/src/main/java/com/fc/shimpyo_be/domain/product/controller/ProductRestController.java index ea18dac5..ddf866a2 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/product/controller/ProductRestController.java +++ b/src/main/java/com/fc/shimpyo_be/domain/product/controller/ProductRestController.java @@ -1,6 +1,7 @@ package com.fc.shimpyo_be.domain.product.controller; import com.fc.shimpyo_be.domain.product.dto.request.SearchKeywordRequest; +import com.fc.shimpyo_be.domain.product.dto.response.PaginatedProductResponse; import com.fc.shimpyo_be.domain.product.dto.response.ProductDetailsResponse; import com.fc.shimpyo_be.domain.product.dto.response.ProductResponse; import com.fc.shimpyo_be.domain.product.entity.Product; @@ -18,6 +19,7 @@ import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -30,39 +32,39 @@ @RequestMapping("/api/products") @RequiredArgsConstructor @Validated +@Transactional(readOnly = true) public class ProductRestController { private final ProductService productService; @GetMapping - public ResponseEntity>> getProducts( - @RequestParam(required = false) String productName, - @RequestParam(required = false) String address, - @RequestParam(required = false) String category, + public ResponseEntity> getProducts( + @RequestParam(required = false, defaultValue = "") String productName, + @RequestParam(required = false, defaultValue = "") String address, + @RequestParam(required = false, defaultValue = "") String category, + @RequestParam(required = false, defaultValue = "0") Long capacity, @PageableConstraint(Product.class) @PageableDefault(size = 10, page = 0) Pageable pageable) { - log.debug("productName: {}, address: {}, category: {}", productName, address, category); SearchKeywordRequest searchKeywordRequest = SearchKeywordRequest.builder() - .productName(productName).address(address).category(category).build(); + .productName(productName).address(address).category(category).capacity(capacity) + .build(); return ResponseEntity.ok(ResponseDto.res(HttpStatus.OK, - productService.getProducts(searchKeywordRequest, pageable), "상품 목록을 성공적으로 조회했습니다.")); + productService.getProducts(searchKeywordRequest, pageable), "숙소 목록을 성공적으로 조회했습니다.")); } - @GetMapping("/{productId}") public ResponseEntity> getProductDetails( @PathVariable("productId") Long productId, @RequestParam @Pattern(regexp = DateTimeUtil.LOCAL_DATE_REGEX_PATTERN, message = "잘못된 시간 형식입니다. (올바른 예시: 2023-10-25)") String startDate, @RequestParam @Pattern(regexp = DateTimeUtil.LOCAL_DATE_REGEX_PATTERN, message = "잘못된 시간 형식입니다. (올바른 예시: 2023-10-25)") String endDate) { - log.debug("productId: {}, startDate: {}, endDate: {}", productId, startDate, endDate); if (DateTimeUtil.isNotValidDate(DateTimeUtil.toLocalDate(startDate), DateTimeUtil.toLocalDate(endDate))) { throw new InvalidDateException(); } return ResponseEntity.ok(ResponseDto.res(HttpStatus.OK, - productService.getProductDetails(productId, startDate, endDate), "상품을 성공적으로 조회했습니다.")); + productService.getProductDetails(productId, startDate, endDate), "숙소을 성공적으로 조회했습니다.")); } @GetMapping("/amounts/{roomId}") @@ -70,7 +72,6 @@ public ResponseEntity> isAvailableForReservation( @PathVariable("roomId") Long roomId, @RequestParam @Pattern(regexp = DateTimeUtil.LOCAL_DATE_REGEX_PATTERN, message = "잘못된 시간 형식입니다. (올바른 예시: 2023-10-25)") String startDate, @RequestParam @Pattern(regexp = DateTimeUtil.LOCAL_DATE_REGEX_PATTERN, message = "잘못된 시간 형식입니다. (올바른 예시: 2023-10-25)") String endDate) { - log.debug("roomId: {}, startDate: {}, endDate: {}", roomId, startDate, endDate); if (DateTimeUtil.isNotValidDate(DateTimeUtil.toLocalDate(startDate), DateTimeUtil.toLocalDate(endDate))) { throw new InvalidDateException(); diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/dto/request/SearchKeywordRequest.java b/src/main/java/com/fc/shimpyo_be/domain/product/dto/request/SearchKeywordRequest.java index f237a0d3..ce530603 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/product/dto/request/SearchKeywordRequest.java +++ b/src/main/java/com/fc/shimpyo_be/domain/product/dto/request/SearchKeywordRequest.java @@ -1,13 +1,27 @@ package com.fc.shimpyo_be.domain.product.dto.request; +import com.fc.shimpyo_be.domain.product.entity.Category; +import jakarta.validation.constraints.NotNull; +import java.util.Arrays; +import java.util.List; import lombok.Builder; -public record SearchKeywordRequest(String productName, String address, String category) { +public record SearchKeywordRequest( + @NotNull + String productName, + @NotNull + String address, + @NotNull + List category, + @NotNull + Long capacity) { + + final static List allCategories = Arrays.stream(Category.values()).toList(); @Builder - public SearchKeywordRequest(String productName, String address, String category) { - this.productName = productName; - this.address = address; - this.category = category; + public SearchKeywordRequest(String productName, String address, String category, + Long capacity) { + this(productName, address, + category.equals("") ? allCategories : List.of(Category.getByName(category)), capacity); } } diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/PaginatedProductResponse.java b/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/PaginatedProductResponse.java new file mode 100644 index 00000000..f328dfdd --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/PaginatedProductResponse.java @@ -0,0 +1,23 @@ +package com.fc.shimpyo_be.domain.product.dto.response; + + +import java.util.ArrayList; +import java.util.List; +import lombok.Builder; + +public record PaginatedProductResponse( + + List productResponses, + int pageCount +) { + + @Builder + public PaginatedProductResponse(List productResponses, int pageCount) { + if (productResponses != null) { + this.productResponses = productResponses; + } else { + this.productResponses = new ArrayList<>(); + } + this.pageCount = pageCount; + } +} \ No newline at end of file diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/ProductAddressResponse.java b/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/ProductAddressResponse.java new file mode 100644 index 00000000..68b2586a --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/ProductAddressResponse.java @@ -0,0 +1,19 @@ +package com.fc.shimpyo_be.domain.product.dto.response; + +import lombok.Builder; + +public record ProductAddressResponse ( + String address, + String detailAddress, + double mapX, + double mapY +) { + + @Builder + public ProductAddressResponse(String address, String detailAddress, double mapX, double mapY) { + this.address = address; + this.detailAddress = detailAddress; + this.mapX = mapX; + this.mapY = mapY; + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/ProductAmenityResponse.java b/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/ProductAmenityResponse.java new file mode 100644 index 00000000..c349176f --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/ProductAmenityResponse.java @@ -0,0 +1,38 @@ +package com.fc.shimpyo_be.domain.product.dto.response; + +import lombok.Builder; + +public record ProductAmenityResponse( + + boolean barbecue, + boolean beauty, + boolean beverage, + boolean bicycle, + boolean campfire, + boolean fitness, + boolean karaoke, + boolean publicBath, + boolean publicPc, + boolean sauna, + boolean sports, + boolean seminar +) { + + @Builder + public ProductAmenityResponse(boolean barbecue, boolean beauty, boolean beverage, boolean bicycle, + boolean campfire, boolean fitness, boolean karaoke, boolean publicBath, boolean publicPc, + boolean sauna, boolean sports, boolean seminar) { + this.barbecue = barbecue; + this.beauty = beauty; + this.beverage = beverage; + this.bicycle = bicycle; + this.campfire = campfire; + this.fitness = fitness; + this.karaoke = karaoke; + this.publicBath = publicBath; + this.publicPc = publicPc; + this.sauna = sauna; + this.sports = sports; + this.seminar = seminar; + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/ProductDetailsResponse.java b/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/ProductDetailsResponse.java index fbc65487..689df9bd 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/ProductDetailsResponse.java +++ b/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/ProductDetailsResponse.java @@ -1,21 +1,25 @@ package com.fc.shimpyo_be.domain.product.dto.response; import com.fc.shimpyo_be.domain.room.dto.response.RoomResponse; +import java.util.ArrayList; import java.util.List; import lombok.Builder; -public record ProductDetailsResponse(Long productId, String category, String address, - String productName, String description, Boolean favorites, - Float starAvg, - +public record ProductDetailsResponse(Long productId, String category, + ProductAddressResponse address, + String productName, + String description, Boolean favorites, Float starAvg, List images, - + ProductAmenityResponse productAmenityResponse, + ProductOptionResponse productOptionResponse, List rooms) { + @Builder - public ProductDetailsResponse(Long productId, String category, String address, + public ProductDetailsResponse(Long productId, String category, ProductAddressResponse address, String productName, String description, Boolean favorites, Float starAvg, - List images, List rooms) { + List images, ProductAmenityResponse productAmenityResponse, + ProductOptionResponse productOptionResponse, List rooms) { this.productId = productId; this.category = category; this.address = address; @@ -23,7 +27,13 @@ public ProductDetailsResponse(Long productId, String category, String address, this.description = description; this.favorites = favorites; this.starAvg = starAvg; - this.images = images; + if (images == null) { + this.images = new ArrayList<>(); + } else { + this.images = images; + } + this.productAmenityResponse = productAmenityResponse; + this.productOptionResponse = productOptionResponse; this.rooms = rooms; } } diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/ProductOptionResponse.java b/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/ProductOptionResponse.java new file mode 100644 index 00000000..0891341a --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/product/dto/response/ProductOptionResponse.java @@ -0,0 +1,32 @@ +package com.fc.shimpyo_be.domain.product.dto.response; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +public record ProductOptionResponse( + boolean cooking, + boolean parking, + boolean pickup, + String foodPlace, + String infoCenter + +) { + @Builder + public ProductOptionResponse(boolean cooking, boolean parking, boolean pickup, + String foodPlace, + String infoCenter) { + this.cooking = cooking; + this.parking = parking; + this.pickup = pickup; + this.foodPlace = foodPlace; + this.infoCenter = infoCenter; + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/entity/Address.java b/src/main/java/com/fc/shimpyo_be/domain/product/entity/Address.java new file mode 100644 index 00000000..5ba7bdee --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/product/entity/Address.java @@ -0,0 +1,44 @@ +package com.fc.shimpyo_be.domain.product.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Address { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment("숙소 위치 식별자") + private Long id; + @Column(nullable = false) + @Comment("숙소 주소") + private String address; + @Column(nullable = false) + @Comment("숙소 상세 주소") + private String detailAddress; + @Column(nullable = false) + @Comment("숙소 X좌표") + private double mapX; + @Column(nullable = false) + @Comment("숙소 Y좌표") + private double mapY; + + @Builder + public Address(Long id, String address, String detailAddress, double mapX, double mapY) { + this.id = id; + this.address = address; + this.detailAddress = detailAddress; + this.mapX = mapX; + this.mapY = mapY; + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/entity/Amenity.java b/src/main/java/com/fc/shimpyo_be/domain/product/entity/Amenity.java new file mode 100644 index 00000000..40c0f9ad --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/product/entity/Amenity.java @@ -0,0 +1,78 @@ +package com.fc.shimpyo_be.domain.product.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Amenity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment("숙소 부대시설 식별자") + private Long id; + @Column(nullable = false) + @Comment("바비큐장 여부") + private boolean barbecue; + @Column(nullable = false) + @Comment("뷰티시설 여부") + private boolean beauty; + @Column(nullable = false) + @Comment("식음료장 여부") + private boolean beverage; + @Column(nullable = false) + @Comment("자전거 대여 여부") + private boolean bicycle; + @Column(nullable = false) + @Comment("캠프파이어 여부") + private boolean campfire; + @Column(nullable = false) + @Comment("휘트니스 센터 여부") + private boolean fitness; + @Column(nullable = false) + @Comment("노래방 여부") + private boolean karaoke; + @Column(nullable = false) + @Comment("공동 샤워실 여부") + private boolean publicBath; + @Column(nullable = false) + @Comment("공동 PC실 여부") + private boolean publicPc; + @Column(nullable = false) + @Comment("사우나 여부") + private boolean sauna; + @Column(nullable = false) + @Comment("스포츠 시설 여부") + private boolean sports; + @Column(nullable = false) + @Comment("세미나실 여부") + private boolean seminar; + + @Builder + public Amenity(Long id, boolean barbecue, boolean beauty, boolean beverage, boolean bicycle, + boolean campfire, boolean fitness, boolean karaoke, boolean publicBath, boolean publicPc, + boolean sauna, boolean sports, boolean seminar) { + this.id = id; + this.barbecue = barbecue; + this.beauty = beauty; + this.beverage = beverage; + this.bicycle = bicycle; + this.campfire = campfire; + this.fitness = fitness; + this.karaoke = karaoke; + this.publicBath = publicBath; + this.publicPc = publicPc; + this.sauna = sauna; + this.sports = sports; + this.seminar = seminar; + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/entity/Product.java b/src/main/java/com/fc/shimpyo_be/domain/product/entity/Product.java index 05b00d2b..f0b332a4 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/product/entity/Product.java +++ b/src/main/java/com/fc/shimpyo_be/domain/product/entity/Product.java @@ -1,5 +1,6 @@ package com.fc.shimpyo_be.domain.product.entity; +import com.fc.shimpyo_be.domain.favorite.entity.Favorite; import com.fc.shimpyo_be.domain.product.util.CategoryConverter; import com.fc.shimpyo_be.domain.room.entity.Room; import jakarta.persistence.CascadeType; @@ -11,6 +12,7 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; import java.util.ArrayList; import java.util.List; import lombok.AccessLevel; @@ -18,6 +20,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.Comment; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -26,28 +29,45 @@ public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment("숙소 식별자") private Long id; @Column(nullable = false) + @Comment("숙소 이름") private String name; - @Column(nullable = false) - private String address; + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @Comment("숙소 위치") + private Address address; @Column(nullable = false) @Convert(converter = CategoryConverter.class) + @Comment("숙소 카테고리") private Category category; @Column(columnDefinition = "TEXT", nullable = false) + @Comment("숙소 설명") private String description; @ColumnDefault("0") + @Comment("숙소 평점") private float starAvg; @Column(columnDefinition = "TEXT", nullable = false) + @Comment("숙소 대표 이미지 URL") private String thumbnail; + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @Comment("숙소 옵션 식별자") + private ProductOption productOption; + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @Comment("숙소 부대시설 식별자") + private Amenity amenity; @OneToMany(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private List photoUrls = new ArrayList<>(); @OneToMany(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private List rooms = new ArrayList<>(); + @OneToMany(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + private List favorites = new ArrayList<>(); + @Builder - public Product(Long id, String name, String address, Category category, String description, - float starAvg, String thumbnail) { + public Product(Long id, String name, Address address, Category category, String description, + float starAvg, String thumbnail, ProductOption productOption, Amenity amenity, + List photoUrls, List rooms, List favorites) { this.id = id; this.name = name; this.address = address; @@ -55,6 +75,11 @@ public Product(Long id, String name, String address, Category category, String d this.description = description; this.starAvg = starAvg; this.thumbnail = thumbnail; + this.productOption = productOption; + this.amenity = amenity; + this.photoUrls = photoUrls; + this.rooms = rooms; + this.favorites = favorites; } public void updateStarAvg(float starAvg) { diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/entity/ProductImage.java b/src/main/java/com/fc/shimpyo_be/domain/product/entity/ProductImage.java index 3370f9e4..86af3946 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/product/entity/ProductImage.java +++ b/src/main/java/com/fc/shimpyo_be/domain/product/entity/ProductImage.java @@ -12,6 +12,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; @Getter @@ -21,17 +22,20 @@ public class ProductImage { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment("숙소 이미지 식별자") private Long id; - @Column(nullable = false, columnDefinition = "TEXT") - private String photoUrl; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(nullable = false, name = "product_id") + @Comment("숙소 식별자") private Product product; + @Column(nullable = false, columnDefinition = "TEXT") + @Comment("숙소 사진 URL") + private String photoUrl; @Builder - public ProductImage(Long id, String photoUrl, Product product) { + public ProductImage(Long id, Product product, String photoUrl) { this.id = id; - this.photoUrl = photoUrl; this.product = product; + this.photoUrl = photoUrl; } } diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/entity/ProductOption.java b/src/main/java/com/fc/shimpyo_be/domain/product/entity/ProductOption.java new file mode 100644 index 00000000..8ec30b43 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/product/entity/ProductOption.java @@ -0,0 +1,50 @@ +package com.fc.shimpyo_be.domain.product.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class ProductOption { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment("숙소 옵션 식별자") + private Long id; + @Column(nullable = false) + @Comment("객실 내 취사 여부") + private boolean cooking; + @Column(nullable = false) + @Comment("주차 시설 여부") + private boolean parking; + @Column(nullable = false) + @Comment("픽업 서비스 여부") + private boolean pickup; + @Column(nullable = false) + @Comment("식음료장") + private String foodPlace; + @Column(nullable = true) + @Comment("문의 및 안내") + private String infoCenter; + + @Builder + public ProductOption(Long id, boolean cooking, boolean parking, boolean pickup, + String foodPlace, + String infoCenter) { + this.id = id; + this.cooking = cooking; + this.parking = parking; + this.pickup = pickup; + this.foodPlace = foodPlace; + this.infoCenter = infoCenter; + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/exception/InvalidDataException.java b/src/main/java/com/fc/shimpyo_be/domain/product/exception/InvalidDataException.java new file mode 100644 index 00000000..312e136b --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/product/exception/InvalidDataException.java @@ -0,0 +1,8 @@ +package com.fc.shimpyo_be.domain.product.exception; + +public class InvalidDataException extends RuntimeException { + + public InvalidDataException(String message) { + super(message); + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/repository/ProductCustomRepository.java b/src/main/java/com/fc/shimpyo_be/domain/product/repository/ProductCustomRepository.java new file mode 100644 index 00000000..a344aa8f --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/product/repository/ProductCustomRepository.java @@ -0,0 +1,12 @@ +package com.fc.shimpyo_be.domain.product.repository; + +import com.fc.shimpyo_be.domain.product.dto.request.SearchKeywordRequest; +import com.fc.shimpyo_be.domain.product.entity.Product; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface ProductCustomRepository { + + Page findAllBySearchKeywordRequest(SearchKeywordRequest searchKeywordRequest, Pageable pageable); + +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/repository/ProductCustomRepositoryImpl.java b/src/main/java/com/fc/shimpyo_be/domain/product/repository/ProductCustomRepositoryImpl.java new file mode 100644 index 00000000..210882ea --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/product/repository/ProductCustomRepositoryImpl.java @@ -0,0 +1,109 @@ +package com.fc.shimpyo_be.domain.product.repository; + + +import static com.fc.shimpyo_be.domain.product.entity.QAddress.address1; +import static com.fc.shimpyo_be.domain.product.entity.QProduct.product; +import static com.fc.shimpyo_be.domain.room.entity.QRoom.room; + +import com.fc.shimpyo_be.domain.product.dto.request.SearchKeywordRequest; +import com.fc.shimpyo_be.domain.product.entity.Product; +import com.fc.shimpyo_be.global.util.QueryDslUtil; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Repository; +import org.springframework.util.ObjectUtils; + + +@Repository +public class ProductCustomRepositoryImpl implements ProductCustomRepository { + + private final JPAQueryFactory queryFactory; + + ProductCustomRepositoryImpl(JPAQueryFactory jpaQueryFactory) { + this.queryFactory = jpaQueryFactory; + } + + public Page findAllBySearchKeywordRequest(SearchKeywordRequest searchKeywordRequest, + Pageable pageable) { + + JPAQuery query = queryFactory + .selectDistinct(product) + .from(product) + .leftJoin(product.rooms, room) + .leftJoin(product.address, address1) + .where(buildSearchConditions(searchKeywordRequest)) + .offset(pageable.getOffset()) + .orderBy(getAllOrderSpecifiers(pageable).toArray(OrderSpecifier[]::new)) + .limit(pageable.getPageSize()); + + JPAQuery countQuery = queryFactory + .selectDistinct(product) + .from(product) + .leftJoin(product.rooms, room) + .leftJoin(product.address, address1) + .where(buildSearchConditions(searchKeywordRequest)); + + List content = query.fetch(); + + return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount()); + } + + private BooleanExpression buildSearchConditions(SearchKeywordRequest searchKeywordRequest) { + List expressions = new ArrayList<>(); + + if (searchKeywordRequest.productName() != null) { + expressions.add(product.name.containsIgnoreCase(searchKeywordRequest.productName())); + } + + if (searchKeywordRequest.address() != null) { + expressions.add( + address1.address.containsIgnoreCase(searchKeywordRequest.address())); + } + + if (searchKeywordRequest.category() != null && !searchKeywordRequest.category().isEmpty()) { + expressions.add(product.category.in(searchKeywordRequest.category())); + } + + if (searchKeywordRequest.capacity() != null) { + expressions.add(room.capacity.goe(searchKeywordRequest.capacity())); + } + + return expressions.stream().reduce(BooleanExpression::and).orElse(null); + } + + + private List> getAllOrderSpecifiers(Pageable pageable) { + + List> ORDERS = new LinkedList<>(); + + if (!ObjectUtils.isEmpty(pageable.getSort())) { + for (Sort.Order order : pageable.getSort()) { + Order direction = order.getDirection().isAscending() ? Order.ASC : Order.DESC; + switch (order.getProperty()) { + case "starAvg" -> { + OrderSpecifier orderId = QueryDslUtil.getSortedColumn(direction, product, + "starAvg"); + ORDERS.add(orderId); + } + } + } + } + + if (ORDERS.isEmpty()) { + ORDERS.add(QueryDslUtil.getSortedColumn(Order.DESC, product, "starAvg")); + } + + return ORDERS; + } + +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/repository/ProductRepository.java b/src/main/java/com/fc/shimpyo_be/domain/product/repository/ProductRepository.java index f5045406..a8be6765 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/product/repository/ProductRepository.java +++ b/src/main/java/com/fc/shimpyo_be/domain/product/repository/ProductRepository.java @@ -2,15 +2,10 @@ import com.fc.shimpyo_be.domain.product.entity.Product; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -public interface ProductRepository extends JpaRepository, - JpaSpecificationExecutor { +public interface ProductRepository extends JpaRepository +{ - Page findAll(Specification spec, Pageable pageable); } diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/repository/model/ProductSpecification.java b/src/main/java/com/fc/shimpyo_be/domain/product/repository/model/ProductSpecification.java deleted file mode 100644 index 199cc25f..00000000 --- a/src/main/java/com/fc/shimpyo_be/domain/product/repository/model/ProductSpecification.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.fc.shimpyo_be.domain.product.repository.model; - -import com.fc.shimpyo_be.domain.product.entity.Category; -import com.fc.shimpyo_be.domain.product.entity.Product; -import org.springframework.data.jpa.domain.Specification; - -public class ProductSpecification { - - public static Specification likeProductName(String productName) { - return (root, query, CriteriaBuilder) -> CriteriaBuilder.like(root.get("name"), - "%" + productName + "%"); - } - - public static Specification equalCategory(String category) { - return (root, query, CriteriaBuilder) -> CriteriaBuilder.equal(root.get("category"), - Category.getByName(category)); - } - - public static Specification likeAddress(String address) { - return (root, query, CriteriaBuilder) -> CriteriaBuilder.like(root.get("address"), - "%" + address + "%"); - } - -} diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/service/OpenApiService.java b/src/main/java/com/fc/shimpyo_be/domain/product/service/OpenApiService.java index 2e924387..63593ce3 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/product/service/OpenApiService.java +++ b/src/main/java/com/fc/shimpyo_be/domain/product/service/OpenApiService.java @@ -1,19 +1,27 @@ package com.fc.shimpyo_be.domain.product.service; +import com.fc.shimpyo_be.domain.product.entity.Address; +import com.fc.shimpyo_be.domain.product.entity.Amenity; import com.fc.shimpyo_be.domain.product.entity.Category; import com.fc.shimpyo_be.domain.product.entity.Product; import com.fc.shimpyo_be.domain.product.entity.ProductImage; +import com.fc.shimpyo_be.domain.product.entity.ProductOption; +import com.fc.shimpyo_be.domain.product.exception.InvalidDataException; import com.fc.shimpyo_be.domain.product.exception.OpenApiException; import com.fc.shimpyo_be.domain.product.repository.ProductImageRepository; import com.fc.shimpyo_be.domain.product.repository.ProductRepository; import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.domain.room.entity.RoomImage; +import com.fc.shimpyo_be.domain.room.entity.RoomOption; +import com.fc.shimpyo_be.domain.room.entity.RoomPrice; +import com.fc.shimpyo_be.domain.room.repository.RoomImageRepository; import com.fc.shimpyo_be.domain.room.repository.RoomRepository; +import jakarta.annotation.PostConstruct; import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.LocalTime; import java.util.ArrayList; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.json.JSONArray; @@ -21,7 +29,9 @@ import org.json.JSONObject; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -43,67 +53,52 @@ public class OpenApiService { private final ProductRepository productRepository; private final ProductImageRepository productImageRepository; private final RoomRepository roomRepository; + private final RoomImageRepository roomImageRepository; private final RestTemplate restTemplate = new RestTemplate(); private HttpEntity httpEntity; + @PostConstruct + public void init() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + httpEntity = new HttpEntity<>(headers); + } + @Transactional public void getData(int pageSize, int pageNum) throws JSONException { try { - JSONObject accommodation = getAccommodation(pageSize, pageNum); - JSONArray base = accommodation - .getJSONObject("items") - .getJSONArray("item"); - for (int j = 0; j < base.length(); j++) { - JSONObject baseItem = base.getJSONObject(j); - int contentId = baseItem.getInt("contentid"); - JSONObject infoBody = getInfo(contentId); - if (isEmpty(infoBody)) { - log.info("반복 정보 조회에 데이터가 없습니다. 다음 숙박 상품을 조회합니다."); - continue; - } - JSONArray info = infoBody.getJSONObject("items") - .getJSONArray("item"); - if (!hasRoom(info)) { - log.info("숙박 상품에 방이 없습니다. 다음 숙박 상품을 조회합니다."); - continue; - } - JSONObject imageBody = getImages(contentId); - if (isEmpty(imageBody)) { - log.info("숙박 상품에 이미지가 없습니다.다음 숙박 상품을 조회합니다."); - continue; - } - JSONArray images = imageBody - .getJSONObject("items") - .getJSONArray("item"); - JSONObject commonBody = getCommon(contentId); - if (isEmpty(commonBody)) { - log.info("공통 정보 조회에 데이터가 없습니다. 다음 숙박 상품을 조회합니다."); - continue; - } - JSONObject common = commonBody.getJSONObject("items") - .getJSONArray("item") - .getJSONObject(0); - JSONObject introBody = getIntro(contentId); - if (isEmpty(introBody)) { - log.info("소개 정보 조회에 데이터가 없습니다. 다음 숙박 상품을 조회합니다."); - continue; - } - JSONObject intro = introBody - .getJSONObject("items") - .getJSONArray("item") - .getJSONObject(0); - if (baseItem.getString("firstimage").isEmpty()) { - log.info("썸네일로 사용할 이미지가 없습니다. 다음 숙박 상품을 조회합니다."); - continue; + JSONArray stayArr = getItems(getAccommodation(pageSize, pageNum)); + + for (int j = 0; j < stayArr.length(); j++) { + try { + JSONObject stay = stayArr.getJSONObject(j); + int contentId = stay.getInt("contentid"); + JSONObject info = getInfo(contentId); + checkInfo(info); + JSONArray rooms = getItems(info); + checkRoom(rooms); + JSONObject image = getImages(contentId); + checkImage(image); + JSONArray images = getItems(image); + JSONObject common = getCommon(contentId); + checkCommon(common); + JSONObject commonItem = getItems(common).getJSONObject(0); + JSONObject intro = getIntro(contentId); + checkIntro(intro); + JSONObject introItem = getItems(intro).getJSONObject(0); + checkIntroItem(introItem); + checkStay(stay); + Product product = saveProduct(stay, commonItem, introItem); + saveProductImages(product, images); + saveRooms(product, introItem, rooms); + } catch (InvalidDataException e) { + log.error(e.getMessage()); } - Product product = saveProduct(baseItem, common); - saveImages(product, images); - saveRooms(product, intro, info); } } catch (Exception e) { - log.error(e.getMessage()); + log.error("[OpenAPI] " + e.getMessage()); throw new OpenApiException(); } } @@ -130,10 +125,7 @@ private JSONObject getAccommodation(int pageSize, int pageNum) throws JSONExcept .build(true).toUri(); ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, httpEntity, String.class); - log.info("숙박 정보 조회"); - return new JSONObject(response.getBody()) - .getJSONObject("response") - .getJSONObject("body"); + return getBody(response.getBody()); } private JSONObject getCommon(long contentId) throws JSONException { @@ -149,10 +141,7 @@ private JSONObject getCommon(long contentId) throws JSONException { .build(true).toUri(); ResponseEntity commonResponse = restTemplate.exchange(uri, HttpMethod.GET, httpEntity, String.class); - log.info(contentId + "번 데이터 공통 정보 조회" + commonResponse.getBody()); - return new JSONObject(commonResponse.getBody()) - .getJSONObject("response") - .getJSONObject("body"); + return getBody(commonResponse.getBody()); } private JSONObject getIntro(long contentId) throws JSONException { @@ -163,10 +152,7 @@ private JSONObject getIntro(long contentId) throws JSONException { .build(true).toUri(); ResponseEntity introResponse = restTemplate.exchange(uri, HttpMethod.GET, httpEntity, String.class); - log.info(contentId + "번 데이터 소개 정보 조회" + introResponse.getBody()); - return new JSONObject(introResponse.getBody()) - .getJSONObject("response") - .getJSONObject("body"); + return getBody(introResponse.getBody()); } private JSONObject getInfo(long contentId) throws JSONException { @@ -177,10 +163,7 @@ private JSONObject getInfo(long contentId) throws JSONException { .build(true).toUri(); ResponseEntity infoResponse = restTemplate.exchange(uri, HttpMethod.GET, httpEntity, String.class); - log.info(contentId + "번 데이터 반복 정보 조회" + infoResponse.getBody()); - return new JSONObject(infoResponse.getBody()) - .getJSONObject("response") - .getJSONObject("body"); + return getBody(infoResponse.getBody()); } private JSONObject getImages(long contentId) throws JSONException { @@ -192,10 +175,15 @@ private JSONObject getImages(long contentId) throws JSONException { .build(true).toUri(); ResponseEntity imageResponse = restTemplate.exchange(uri, HttpMethod.GET, httpEntity, String.class); - log.info(contentId + "번 데이터 이미지 정보 조회" + imageResponse.getBody()); - return new JSONObject(imageResponse.getBody()) - .getJSONObject("response") - .getJSONObject("body"); + return getBody(imageResponse.getBody()); + } + + private JSONArray getItems(JSONObject jsonObject) { + return jsonObject.getJSONObject("items").getJSONArray("item"); + } + + private JSONObject getBody(String source) { + return new JSONObject(source).getJSONObject("response").getJSONObject("body"); } private boolean hasRoom(JSONArray info) throws JSONException { @@ -208,65 +196,183 @@ private boolean hasRoom(JSONArray info) throws JSONException { return hasRoom; } - private Product saveProduct(JSONObject base, JSONObject common) throws JSONException { + private Product saveProduct(JSONObject base, JSONObject common, JSONObject intro) + throws JSONException { + ProductOption productOption = ProductOption.builder() + .cooking(intro.get("chkcooking").equals("가능")) + .parking(intro.get("parkinglodging").equals("가능")) + .pickup(intro.get("pickup").equals("가능")) + .foodPlace(intro.getString("foodplace")) + .infoCenter(intro.getString("infocenterlodging")) + .build(); + Amenity amenity = Amenity.builder() + .barbecue(intro.get("barbecue").equals("1")) + .beauty(intro.get("beauty").equals("1")) + .beverage(intro.get("beverage").equals("1")) + .bicycle(intro.get("bicycle").equals("1")) + .campfire(intro.get("campfire").equals("1")) + .fitness(intro.get("fitness").equals("1")) + .karaoke(intro.get("karaoke").equals("1")) + .publicBath(intro.get("publicbath").equals("1")) + .publicPc(intro.get("publicpc").equals("1")) + .sauna(intro.get("sauna").equals("1")) + .sports(intro.get("sports").equals("1")) + .seminar(intro.get("seminar").equals("1")) + .build(); Product product = Product.builder() .name(base.getString("title")) + .address( + Address.builder() + .address(base.getString("addr1")) + .detailAddress(base.getString("addr2")) + .mapX(base.getDouble("mapx")) + .mapY(base.getDouble("mapy")) + .build() + ) .category(Category.getByCode(base.getString("cat3"))) - .address(base.getString("addr1") + " " + base.getString("addr2")) .description(common.getString("overview")) .starAvg(0) .thumbnail(base.getString("firstimage")) + .photoUrls(new ArrayList<>()) + .productOption(productOption) + .amenity(amenity) .build(); return productRepository.save(product); } - private void saveImages(Product product, JSONArray images) throws JSONException { - List productImages = new ArrayList<>(); + private void saveProductImages(Product product, JSONArray images) { for (int k = 0; k < images.length(); k++) { - productImages.add(ProductImage.builder() + productImageRepository.save(ProductImage.builder() .product(product) .photoUrl(images.getJSONObject(k).getString("originimgurl")) .build()); } - productImageRepository.saveAll(productImages); } private void saveRooms(Product product, JSONObject intro, JSONArray info) throws JSONException { - List rooms = new ArrayList<>(); - for (int k = 0; k < info.length(); k++) { - JSONObject roomJson = info.getJSONObject(k); + for (int i = 0; i < info.length(); i++) { + JSONObject roomJson = info.getJSONObject(i); + if (roomJson.getInt("roombasecount") == 0 && roomJson.getInt("roommaxcount") == 0) { + continue; + } if (Integer.parseInt(roomJson.getString("roomcount")) != 0) { - for (int r = 0; r < Integer.parseInt(roomJson.getString("roomcount")); - r++) { - System.out.println(intro.toString()); - String[] checkIn = intro.getString("checkintime").split(":|;"); - String[] checkOut = intro.getString("checkouttime").split(":|;"); - System.out.println(roomJson); - rooms.add(Room.builder() + for (int j = 0; j < Integer.parseInt(roomJson.getString("roomcount")); j++) { + String[] stringCheckIn = intro.getString("checkintime").split(":|;|시"); + String[] stringCheckOut = intro.getString("checkouttime").split(":|;|시"); + LocalTime checkIn = getTimeFromString(stringCheckIn); + LocalTime checkOut = getTimeFromString(stringCheckOut); + + RoomPrice roomPrice = RoomPrice.builder() + .offWeekDaysMinFee(Integer.parseInt( + roomJson.getString("roomoffseasonminfee1"))) + .offWeekendMinFee(Integer.parseInt( + roomJson.getString("roomoffseasonminfee2"))) + .peakWeekDaysMinFee(Integer.parseInt( + roomJson.getString("roompeakseasonminfee1"))) + .peakWeekendMinFee(Integer.parseInt( + roomJson.getString("roompeakseasonminfee2"))) + .build(); + RoomOption roomOption = RoomOption.builder() + .bathFacility(roomJson.get("roombathfacility").equals("Y")) + .bath(roomJson.get("roombath").equals("Y")) + .homeTheater(roomJson.get("roomhometheater").equals("Y")) + .airCondition(roomJson.get("roomaircondition").equals("Y")) + .tv(roomJson.get("roomtv").equals("Y")) + .pc(roomJson.get("roompc").equals("Y")) + .cable(roomJson.get("roomcable").equals("Y")) + .internet(roomJson.get("roominternet").equals("Y")) + .refrigerator(roomJson.get("roomrefrigerator").equals("Y")) + .toiletries(roomJson.get("roomtoiletries").equals("Y")) + .sofa(roomJson.get("roomsofa").equals("Y")) + .cooking(roomJson.get("roomcook").equals("Y")) + .diningTable(roomJson.get("roomtable").equals("Y")) + .hairDryer(roomJson.get("roomhairdryer").equals("Y")) + .build(); + Room room = roomRepository.save(Room.builder() .product(product) + .code(roomJson.getLong("roomcode")) .name(roomJson.getString("roomtitle")) .description(roomJson.getString("roomintro")) .standard( roomJson.getInt("roombasecount")) .capacity(Math.max(roomJson.getInt("roombasecount"), roomJson.getInt("roommaxcount"))) - .checkIn(LocalTime.of( - Integer.parseInt(checkIn[0].substring(checkIn[0].length() - 2)), - Integer.parseInt(checkIn[1].substring(0, 2)))) - .checkOut(LocalTime.of( - Integer.parseInt(checkOut[0].substring(checkOut[0].length() - 2)), - Integer.parseInt(checkOut[1].substring(0, 2)))) - .price( - Integer.parseInt( - roomJson.getString("roompeakseasonminfee1"))) + .checkIn(checkIn) + .checkOut(checkOut) + .price(roomPrice) + .roomOption(roomOption) + .roomImages(new ArrayList<>()) .build()); + for (int k = 1; k <= 5; k++) { + if (!roomJson.get("roomimg" + k).equals("")) { + roomImageRepository.save(RoomImage.builder() + .room(room) + .photoUrl(roomJson.getString("roomimg" + k)) + .description(roomJson.getString("roomimg" + k + "alt")) + .build()); + } + } } } } - roomRepository.saveAll(rooms); } private boolean isEmpty(JSONObject body) throws JSONException { return body.getInt("totalCount") == 0; } + + private LocalTime getTimeFromString(String[] stringTime) { + int hour = Integer.parseInt( + stringTime[0].trim().substring(stringTime[0].trim().length() - 2)); + int minute = + stringTime.length == 1 ? 0 : Integer.parseInt(stringTime[1].trim().substring(0, 2)); + return LocalTime.of(hour, minute); + } + + private void checkInfo(JSONObject info) { + if (isEmpty(info)) { + throw new InvalidDataException("반복 정보 조회에 데이터가 없습니다. 다음 숙소를 조회합니다."); + } + } + + private void checkRoom(JSONArray rooms) { + if (!hasRoom(rooms)) { + throw new InvalidDataException("숙박 숙소에 방이 없습니다. 다음 숙소를 조회합니다."); + } + } + + private void checkImage(JSONObject image) { + if (isEmpty(image)) { + throw new InvalidDataException("숙박 숙소에 이미지가 없습니다.다음 숙소를 조회합니다."); + } + } + + private void checkCommon(JSONObject common) { + if (isEmpty(common)) { + throw new InvalidDataException("공통 정보 조회에 데이터가 없습니다. 다음 숙소를 조회합니다."); + } + } + + private void checkIntro(JSONObject intro) { + if (isEmpty(intro)) { + throw new InvalidDataException("소개 정보 조회에 데이터가 없습니다. 다음 숙소를 조회합니다."); + } + } + + private void checkIntroItem(JSONObject introItem) { + if (introItem.getString("checkintime").trim().isEmpty() || introItem.getString( + "checkouttime").trim().isEmpty()) { + throw new InvalidDataException("체크인 체크아웃 데이터가 없습니다. 다음 숙소를 조회합니다. "); + } + if (introItem.getString("checkintime").split(":|;|시").length != 2 + || introItem.getString("checkouttime").split(":|;|시").length != 2) { + throw new InvalidDataException("체크인 체크아웃 데이터가 형식에 맞지 않습니다. 다음 숙소를 조회합니다."); + } + } + + private void checkStay(JSONObject stay) { + if (stay.getString("firstimage").isEmpty()) { + throw new InvalidDataException("썸네일로 사용할 이미지가 없습니다. 다음 숙소를 조회합니다."); + } + } } diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/service/ProductService.java b/src/main/java/com/fc/shimpyo_be/domain/product/service/ProductService.java index aab0caad..5b28b31c 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/product/service/ProductService.java +++ b/src/main/java/com/fc/shimpyo_be/domain/product/service/ProductService.java @@ -1,22 +1,27 @@ package com.fc.shimpyo_be.domain.product.service; +import com.fc.shimpyo_be.domain.favorite.entity.Favorite; import com.fc.shimpyo_be.domain.product.dto.request.SearchKeywordRequest; +import com.fc.shimpyo_be.domain.product.dto.response.PaginatedProductResponse; import com.fc.shimpyo_be.domain.product.dto.response.ProductDetailsResponse; import com.fc.shimpyo_be.domain.product.dto.response.ProductResponse; import com.fc.shimpyo_be.domain.product.entity.Product; import com.fc.shimpyo_be.domain.product.exception.ProductNotFoundException; +import com.fc.shimpyo_be.domain.product.repository.ProductCustomRepositoryImpl; import com.fc.shimpyo_be.domain.product.repository.ProductRepository; -import com.fc.shimpyo_be.domain.product.repository.model.ProductSpecification; import com.fc.shimpyo_be.domain.product.util.ProductMapper; -import com.fc.shimpyo_be.domain.room.dto.response.RoomResponse; +import com.fc.shimpyo_be.domain.room.entity.Room; import com.fc.shimpyo_be.domain.room.repository.RoomRepository; import com.fc.shimpyo_be.global.util.DateTimeUtil; +import com.fc.shimpyo_be.global.util.SecurityUtil; import java.time.LocalDate; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.domain.Specification; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Service; @@ -27,51 +32,57 @@ public class ProductService { private final ProductRepository productRepository; + private final ProductCustomRepositoryImpl productCustomRepository; + private final RoomRepository roomRepository; private final RedisTemplate restTemplate; - public List getProducts(final SearchKeywordRequest searchKeywordRequest, + private final SecurityUtil securityUtil; + + public PaginatedProductResponse getProducts(final SearchKeywordRequest searchKeywordRequest, final Pageable pageable) { - Specification spec = (root, query, criteriaBuilder) -> null; + Page products = Optional.of( + productCustomRepository.findAllBySearchKeywordRequest(searchKeywordRequest, pageable)) + .orElseThrow(); - if (searchKeywordRequest.productName() != null) { - spec = spec.and( - ProductSpecification.likeProductName(searchKeywordRequest.productName())); - } - if (searchKeywordRequest.category() != null) { - if (searchKeywordRequest.category().contains(",")) { - String[] categories = searchKeywordRequest.category().split(","); - for (int i = 0; i < categories.length; i++) { - spec = spec.or(ProductSpecification.equalCategory(categories[i])); - } - } else { - spec = spec.and( - ProductSpecification.equalCategory(searchKeywordRequest.category())); - } - } - if (searchKeywordRequest.address() != null) { - spec = spec.and(ProductSpecification.likeAddress(searchKeywordRequest.address())); - } - - return Optional.of(productRepository.findAll(spec, pageable)).orElseThrow().getContent() - .stream().map(ProductMapper::toProductResponse).toList(); + return PaginatedProductResponse.builder() + .productResponses( + getProductResponseSettingFavorites(products.getContent())) + .pageCount(products.getTotalPages()) + .build(); } public ProductDetailsResponse getProductDetails(final Long productId, final String startDate, final String endDate) { Product product = productRepository.findById(productId) .orElseThrow(ProductNotFoundException::new); + HashSet favoriteProductIds = getFavoriteProductIds(List.of(product)); ProductDetailsResponse productDetailsResponse = ProductMapper.toProductDetailsResponse( - product); - productDetailsResponse.rooms().stream().filter( - roomResponse -> !isAvailableForReservation(roomResponse.getRoomId(), startDate, - endDate)) - .forEach(RoomResponse::setReserved); + product, favoriteProductIds.contains(product.getId())); + + productDetailsResponse.rooms().forEach( + roomResponse -> roomResponse.setRemaining( + countAvailableForReservationUsingRoomCode(roomResponse.getRoomCode(), startDate, + endDate))); return productDetailsResponse; } + public long countAvailableForReservationUsingRoomCode(final Long roomCode, + final String startDate, + final String endDate) { + AtomicLong remaining = new AtomicLong(); + List rooms = Optional.of(roomRepository.findByCode(roomCode)).orElseThrow(); + rooms.forEach(room -> { + if (isAvailableForReservation(room.getId(), startDate, endDate)) { + remaining.getAndIncrement(); + } + }); + + return remaining.get(); + } + public boolean isAvailableForReservation(final Long roomId, final String startDate, final String endDate) { ValueOperations values = restTemplate.opsForValue(); @@ -82,7 +93,7 @@ public boolean isAvailableForReservation(final Long roomId, final String startDa while (startLocalDate.isBefore(endLocalDate)) { String accommodationDate = DateTimeUtil.toString(startLocalDate); - if (values.get("roomId:" + String.valueOf(roomId) + ":" + accommodationDate) != null) { + if (values.get("roomId:" + roomId + ":" + accommodationDate) != null) { return false; } startLocalDate = startLocalDate.plusDays(1); @@ -91,5 +102,31 @@ public boolean isAvailableForReservation(final Long roomId, final String startDa return true; } + private List getProductResponseSettingFavorites(List products) { + + HashSet favoriteProductIds = getFavoriteProductIds(products); + + return products.stream().map(product -> ProductMapper.toProductResponse(product, + favoriteProductIds.contains(product.getId()))).toList(); + + } + + private HashSet getFavoriteProductIds(List products) { + Long userId = securityUtil.getNullableCurrentMemberId(); + HashSet favoriteProductId = new HashSet<>(); + if (userId != null) { + for (Product product : products) { + for (Favorite favorite : product.getFavorites()) { + if (favorite.getMember().getId().equals(userId)) { + favoriteProductId.add(product.getId()); + break; + } + } + } + } + + return favoriteProductId; + } + } diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/util/ProductMapper.java b/src/main/java/com/fc/shimpyo_be/domain/product/util/ProductMapper.java index 53dba4c4..b7b5d56e 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/product/util/ProductMapper.java +++ b/src/main/java/com/fc/shimpyo_be/domain/product/util/ProductMapper.java @@ -1,51 +1,112 @@ package com.fc.shimpyo_be.domain.product.util; +import com.fc.shimpyo_be.domain.product.dto.response.ProductAddressResponse; +import com.fc.shimpyo_be.domain.product.dto.response.ProductAmenityResponse; import com.fc.shimpyo_be.domain.product.dto.response.ProductDetailsResponse; +import com.fc.shimpyo_be.domain.product.dto.response.ProductOptionResponse; import com.fc.shimpyo_be.domain.product.dto.response.ProductResponse; +import com.fc.shimpyo_be.domain.product.entity.Address; +import com.fc.shimpyo_be.domain.product.entity.Amenity; import com.fc.shimpyo_be.domain.product.entity.Product; import com.fc.shimpyo_be.domain.product.entity.ProductImage; +import com.fc.shimpyo_be.domain.product.entity.ProductOption; import com.fc.shimpyo_be.domain.room.entity.Room; import com.fc.shimpyo_be.domain.room.util.RoomMapper; +import com.fc.shimpyo_be.global.util.PricePickerByDateUtil; import java.util.ArrayList; import java.util.List; public class ProductMapper { - public static ProductResponse toProductResponse(Product product) { + + public static ProductResponse toProductResponse(Product product, boolean isFavorite) { + return ProductResponse.builder().productId(product.getId()).productName(product.getName()) - .address(product.getAddress()).category(product.getCategory().getName()) + .address( + product.getAddress().getAddress() + " " + product.getAddress().getDetailAddress()) + .category(product.getCategory().getName()) .image(product.getThumbnail()) .starAvg(product.getStarAvg()) - .price(product.getRooms().isEmpty() - ? 0 : Long.valueOf( - product.getRooms().stream().map(Room::getPrice).min((o1, o2) -> o1 - o2) - .orElseThrow())) + .price(getPrice(product)) .capacity(product.getRooms().isEmpty() ? 0 : Long.valueOf( product.getRooms().stream().map(Room::getCapacity).min((o1, o2) -> o2 - o1) .orElseThrow())) - .favorites(false) + .favorites(isFavorite) .build(); } - public static ProductDetailsResponse toProductDetailsResponse(Product product) { - - List images = new ArrayList<>(); - images.add(product.getThumbnail()); - images.addAll(product.getPhotoUrls().stream().map(ProductImage::getPhotoUrl).toList()); + public static ProductDetailsResponse toProductDetailsResponse(Product product, boolean isFavorite) { return ProductDetailsResponse.builder() .productId(product.getId()) .category(product.getCategory().getName()) - .address(product.getAddress()) + .address(toProductAddressResponse(product.getAddress())) .productName(product.getName()) .description(product.getDescription()) + .productAmenityResponse(toProductAmenityResponse(product.getAmenity())) .starAvg(product.getStarAvg()) - .favorites(false) - .images(images) - .rooms(product.getRooms().stream().map(RoomMapper::from).toList()) + .productOptionResponse(toProductOptionResponse(product.getProductOption())) + .favorites(isFavorite) + .images(getImage(product)) + .rooms(product.getRooms().stream().map(RoomMapper::toRoomResponse).distinct().toList()) + .build(); + } + + private static ProductAddressResponse toProductAddressResponse(Address address) { + return ProductAddressResponse.builder() + .address(address.getAddress()) + .detailAddress(address.getDetailAddress()) + .mapX(address.getMapX()) + .mapY(address.getMapY()) .build(); } + private static ProductAmenityResponse toProductAmenityResponse(Amenity amenity) { + return ProductAmenityResponse.builder() + .barbecue(amenity.isBarbecue()) + .bicycle(amenity.isBicycle()) + .beauty(amenity.isBeauty()) + .beverage(amenity.isBeverage()) + .sauna(amenity.isSauna()) + .fitness(amenity.isFitness()) + .karaoke(amenity.isKaraoke()) + .publicBath(amenity.isPublicBath()) + .publicPc(amenity.isPublicPc()) + .seminar(amenity.isSeminar()) + .sports(amenity.isSports()) + .campfire(amenity.isCampfire()) + .build(); + } + + private static ProductOptionResponse toProductOptionResponse(ProductOption productOption) { + return ProductOptionResponse.builder() + .pickup(productOption.isPickup()) + .parking(productOption.isParking()) + .cooking(productOption.isCooking()) + .infoCenter(productOption.getInfoCenter()) + .foodPlace(productOption.getFoodPlace()) + .build(); + } + + private static long getPrice(Product product) { + List rooms = product.getRooms(); + long price = rooms.isEmpty() ? 0 : rooms.stream().map(PricePickerByDateUtil::getPrice) + .min((o1, o2) -> Math.toIntExact( + o1 - o2)).orElseThrow(); + + return price == 0 ? 100000 : price; + } + + private static List getImage(Product product) { + List images = new ArrayList<>(); + images.add(product.getThumbnail()); + + if (product.getPhotoUrls() != null) { + images.addAll(product.getPhotoUrls().stream().map(ProductImage::getPhotoUrl).toList()); + } + + return images; + } } diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/constant/ReservationValidationConstants.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/constant/ReservationValidationConstants.java new file mode 100644 index 00000000..98d07503 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/constant/ReservationValidationConstants.java @@ -0,0 +1,20 @@ +package com.fc.shimpyo_be.domain.reservation.constant; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ReservationValidationConstants { + + // regex + public static final String DATE_REGEX = "^\\d{4}-\\d{2}-\\d{2}$"; + public static final int RESERVATION_REQ_MIN_SIZE = 1; + public static final int RESERVATION_REQ_MAX_SIZE = 3; + public static final int TOTAL_PRICE_MIN_VALUE = 0; + + // validation message + public static final String DATE_PATTERN_MESSAGE = "올바른 날짜 형식이 아닙니다.(yyyy-MM-dd 형식으로 입력하세요.)"; + public static final String RESERVATION_REQ_SIZE_MESSAGE = "최소 1개, 최대 3개의 객실 예약이 가능합니다."; + public static final String PAYMETHOD_NOTNULL_MESSAGE = "null 일 수 없습니다. 정해진 결제 수단에서 선택하세요."; + public static final String TOTAL_PRICE_MIN_MESSAGE = "총 결제 금액은 최소 0원 이상이어야 합니다."; +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/controller/ReservationRestController.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/controller/ReservationRestController.java index 4af7a175..fbce2b09 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/controller/ReservationRestController.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/controller/ReservationRestController.java @@ -3,7 +3,9 @@ import com.fc.shimpyo_be.domain.reservation.dto.request.PreoccupyRoomsRequestDto; import com.fc.shimpyo_be.domain.reservation.dto.request.ReleaseRoomsRequestDto; import com.fc.shimpyo_be.domain.reservation.dto.request.SaveReservationRequestDto; +import com.fc.shimpyo_be.domain.reservation.dto.response.ReservationInfoResponseDto; import com.fc.shimpyo_be.domain.reservation.dto.response.SaveReservationResponseDto; +import com.fc.shimpyo_be.domain.reservation.dto.response.ValidatePreoccupyResultResponseDto; import com.fc.shimpyo_be.domain.reservation.facade.PreoccupyRoomsLockFacade; import com.fc.shimpyo_be.domain.reservation.facade.ReservationLockFacade; import com.fc.shimpyo_be.domain.reservation.service.ReservationService; @@ -30,12 +32,14 @@ public class ReservationRestController { private final PreoccupyRoomsLockFacade preoccupyRoomsLockFacade; private final ReservationLockFacade reservationLockFacade; private final SecurityUtil securityUtil; + private static final int PAGE_SIZE = 10; + private static final int PAGE_NUM = 0; + private static final String PAGE_SORT_BY = "id"; @PostMapping public ResponseEntity> saveReservation( @Valid @RequestBody SaveReservationRequestDto request ) { - log.info("[api][POST] /api/reservations"); return ResponseEntity .status(HttpStatus.CREATED) @@ -49,10 +53,9 @@ public ResponseEntity> saveReservation( } @GetMapping - public ResponseEntity>> getReservationList( - @PageableDefault(size = 10, page = 0, sort = "id", direction = Sort.Direction.DESC) Pageable pageable + public ResponseEntity>> getReservationList( + @PageableDefault(size = PAGE_SIZE, page = PAGE_NUM, sort = PAGE_SORT_BY, direction = Sort.Direction.DESC) Pageable pageable ) { - log.info("[api][GET] /api/reservations"); return ResponseEntity .status(HttpStatus.OK) @@ -66,17 +69,18 @@ public ResponseEntity>> getReservationList( } @PostMapping("/preoccupy") - public ResponseEntity> checkAvailableAndPreoccupy( + public ResponseEntity> checkAvailableAndPreoccupy( @Valid @RequestBody PreoccupyRoomsRequestDto request ) { - log.info("[api][POST] /api/reservations/preoccupy"); - - preoccupyRoomsLockFacade.checkAvailableAndPreoccupy(securityUtil.getCurrentMemberId(), request); return ResponseEntity .status(HttpStatus.OK) .body( - ResponseDto.res(HttpStatus.OK, "예약 가능 유효성 검사와 객실 선점이 정상적으로 완료되었습니다.") + ResponseDto.res( + HttpStatus.OK, + preoccupyRoomsLockFacade.checkAvailableAndPreoccupy(securityUtil.getCurrentMemberId(), request), + "예약 가능 유효성 검사와 객실 선점이 정상적으로 완료되었습니다." + ) ); } @@ -84,7 +88,6 @@ public ResponseEntity> checkAvailableAndPreoccupy( public ResponseEntity> releaseRooms( @Valid @RequestBody ReleaseRoomsRequestDto request ) { - log.info("[api][POST] /api/reservations/release"); reservationService.releaseRooms(securityUtil.getCurrentMemberId(), request); diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/CheckAvailableRoomsResultDto.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/CheckAvailableRoomsResultDto.java index 149b0e65..7f8ef272 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/CheckAvailableRoomsResultDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/CheckAvailableRoomsResultDto.java @@ -1,11 +1,15 @@ package com.fc.shimpyo_be.domain.reservation.dto; +import com.fc.shimpyo_be.domain.reservation.dto.response.ValidatePreoccupyRoomResponseDto; +import lombok.Builder; + import java.util.List; import java.util.Map; +@Builder public record CheckAvailableRoomsResultDto( boolean isAvailable, - List unavailableIds, - Map> recordMap + List roomResults, + Map> preoccupyMap ) { } diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/ValidateReservationResultDto.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/ValidateReservationResultDto.java new file mode 100644 index 00000000..87e3ebf2 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/ValidateReservationResultDto.java @@ -0,0 +1,14 @@ +package com.fc.shimpyo_be.domain.reservation.dto; + +import lombok.Builder; + +import java.util.List; +import java.util.Map; + +@Builder +public record ValidateReservationResultDto( + boolean isAvailable, + List unavailableIds, + Map> confirmMap +) { +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/PreoccupyRoomItemRequestDto.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/PreoccupyRoomItemRequestDto.java index 1c001d79..9d6aa745 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/PreoccupyRoomItemRequestDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/PreoccupyRoomItemRequestDto.java @@ -2,13 +2,19 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; +import lombok.Builder; +import static com.fc.shimpyo_be.domain.reservation.constant.ReservationValidationConstants.*; + +@Builder public record PreoccupyRoomItemRequestDto( @NotNull - Long roomId, - @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "올바른 날짜 형식이 아닙니다.(yyyy-MM-dd 형식으로 입력하세요.)") + Long cartId, + @NotNull + Long roomCode, + @Pattern(regexp = DATE_REGEX, message = DATE_PATTERN_MESSAGE) String startDate, - @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "올바른 날짜 형식이 아닙니다.(yyyy-MM-dd 형식으로 입력하세요.)") + @Pattern(regexp = DATE_REGEX, message = DATE_PATTERN_MESSAGE) String endDate ) { } diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/PreoccupyRoomsRequestDto.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/PreoccupyRoomsRequestDto.java index 251b1c6a..35080845 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/PreoccupyRoomsRequestDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/PreoccupyRoomsRequestDto.java @@ -2,13 +2,16 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.Size; +import lombok.Builder; import java.util.List; -public record PreoccupyRoomsRequestDto( +import static com.fc.shimpyo_be.domain.reservation.constant.ReservationValidationConstants.*; +@Builder +public record PreoccupyRoomsRequestDto( @Valid - @Size(min = 1, max = 3, message = "최소 1개, 최대 3개의 객실 예약이 가능합니다.") + @Size(min = RESERVATION_REQ_MIN_SIZE, max = RESERVATION_REQ_MAX_SIZE, message = RESERVATION_REQ_SIZE_MESSAGE) List rooms ) { } diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/ReleaseRoomItemRequestDto.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/ReleaseRoomItemRequestDto.java index 448aac87..46c2d3d7 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/ReleaseRoomItemRequestDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/ReleaseRoomItemRequestDto.java @@ -2,13 +2,17 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; +import lombok.Builder; +import static com.fc.shimpyo_be.domain.reservation.constant.ReservationValidationConstants.*; + +@Builder public record ReleaseRoomItemRequestDto( @NotNull Long roomId, - @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "올바른 날짜 형식이 아닙니다.(yyyy-MM-dd 형식으로 입력하세요.)") + @Pattern(regexp = DATE_REGEX, message = DATE_PATTERN_MESSAGE) String startDate, - @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "올바른 날짜 형식이 아닙니다.(yyyy-MM-dd 형식으로 입력하세요.)") + @Pattern(regexp = DATE_REGEX, message = DATE_PATTERN_MESSAGE) String endDate ) { } diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/ReleaseRoomsRequestDto.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/ReleaseRoomsRequestDto.java index 00a295f9..d421e828 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/ReleaseRoomsRequestDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/ReleaseRoomsRequestDto.java @@ -2,12 +2,16 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.Size; +import lombok.Builder; import java.util.List; +import static com.fc.shimpyo_be.domain.reservation.constant.ReservationValidationConstants.*; + +@Builder public record ReleaseRoomsRequestDto( @Valid - @Size(min = 1, max = 3, message = "최소 1개, 최대 3개의 객실 요청 정보가 필요합니다.") + @Size(min = RESERVATION_REQ_MIN_SIZE, max = RESERVATION_REQ_MAX_SIZE, message = RESERVATION_REQ_SIZE_MESSAGE) List rooms ) { } diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/SaveReservationRequestDto.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/SaveReservationRequestDto.java index 93fde17e..220cf3bc 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/SaveReservationRequestDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/request/SaveReservationRequestDto.java @@ -6,16 +6,20 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import lombok.Builder; import java.util.List; +import static com.fc.shimpyo_be.domain.reservation.constant.ReservationValidationConstants.*; + +@Builder public record SaveReservationRequestDto( @Valid - @Size(min = 1, max = 3, message = "최소 1개, 최대 3개의 객실 예약이 가능합니다.") + @Size(min = RESERVATION_REQ_MIN_SIZE, max = RESERVATION_REQ_MAX_SIZE, message = RESERVATION_REQ_SIZE_MESSAGE) List reservationProducts, - @NotNull(message = "null 일 수 없습니다. 정해진 결제 수단에서 선택하세요.") + @NotNull(message = PAYMETHOD_NOTNULL_MESSAGE) PayMethod payMethod, - @Min(value = 0, message = "총 결제 금액은 음수일 수 없습니다.") + @Min(value = TOTAL_PRICE_MIN_VALUE, message = TOTAL_PRICE_MIN_MESSAGE) Integer totalPrice ) { } diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ReservationInfoResponseDto.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ReservationInfoResponseDto.java index 373696f3..ecb80459 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ReservationInfoResponseDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ReservationInfoResponseDto.java @@ -1,5 +1,8 @@ package com.fc.shimpyo_be.domain.reservation.dto.response; +import lombok.Builder; + +@Builder public record ReservationInfoResponseDto( Long reservationId, Long reservationProductId, @@ -7,6 +10,7 @@ public record ReservationInfoResponseDto( String productName, String productImageUrl, String productAddress, + String productDetailAddress, Long roomId, String roomName, String startDate, @@ -14,6 +18,7 @@ public record ReservationInfoResponseDto( String checkIn, String checkOut, Integer price, - String payMethod + String payMethod, + String createdAt ) { } diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/SaveReservationResponseDto.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/SaveReservationResponseDto.java index e679dbed..531b47ba 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/SaveReservationResponseDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/SaveReservationResponseDto.java @@ -1,25 +1,17 @@ package com.fc.shimpyo_be.domain.reservation.dto.response; -import com.fc.shimpyo_be.domain.reservation.dto.request.SaveReservationRequestDto; import com.fc.shimpyo_be.domain.reservation.entity.PayMethod; import com.fc.shimpyo_be.domain.reservationproduct.dto.response.ReservationProductResponseDto; +import lombok.Builder; import java.util.List; +@Builder public record SaveReservationResponseDto( Long reservationId, List reservationProducts, PayMethod payMethod, - Integer totalPrice + Integer totalPrice, + String createdAt ) { - public SaveReservationResponseDto(Long reservationId, SaveReservationRequestDto requestDto) { - this( - reservationId, - requestDto.reservationProducts() - .stream() - .map(ReservationProductResponseDto::new).toList(), - requestDto.payMethod(), - requestDto.totalPrice() - ); - } } diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ValidatePreoccupyResultResponseDto.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ValidatePreoccupyResultResponseDto.java new file mode 100644 index 00000000..4eb8289b --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ValidatePreoccupyResultResponseDto.java @@ -0,0 +1,12 @@ +package com.fc.shimpyo_be.domain.reservation.dto.response; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record ValidatePreoccupyResultResponseDto( + boolean isAvailable, + List roomResults +) { +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ValidatePreoccupyRoomResponseDto.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ValidatePreoccupyRoomResponseDto.java new file mode 100644 index 00000000..92cef81c --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ValidatePreoccupyRoomResponseDto.java @@ -0,0 +1,13 @@ +package com.fc.shimpyo_be.domain.reservation.dto.response; + +import lombok.Builder; + +@Builder +public record ValidatePreoccupyRoomResponseDto( + Long cartId, + Long roomCode, + String startDate, + String endDate, + Long roomId +) { +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ValidationResultResponseDto.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ValidateReservationResultResponseDto.java similarity index 63% rename from src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ValidationResultResponseDto.java rename to src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ValidateReservationResultResponseDto.java index 516d2815..d84abc18 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ValidationResultResponseDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/dto/response/ValidateReservationResultResponseDto.java @@ -1,8 +1,11 @@ package com.fc.shimpyo_be.domain.reservation.dto.response; +import lombok.Builder; + import java.util.List; -public record ValidationResultResponseDto( +@Builder +public record ValidateReservationResultResponseDto( boolean isAvailable, List unavailableIds ) { diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/entity/Reservation.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/entity/Reservation.java index 4d5c4cc6..c904a365 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/entity/Reservation.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/entity/Reservation.java @@ -8,6 +8,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; import org.springframework.util.ObjectUtils; import java.util.ArrayList; @@ -20,13 +21,17 @@ public class Reservation extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment(value = "예약 식별자") private Long id; + @Comment(value = "예약 회원 식별자") @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; + @Comment(value = "결제 수단") @Column(nullable = false) @Enumerated(value = EnumType.STRING) private PayMethod payMethod; + @Comment(value = "총 결제 금액") @Column(nullable = false) private int totalPrice; @@ -34,7 +39,7 @@ public class Reservation extends BaseTimeEntity { private List reservationProducts = new ArrayList<>(); @Builder - public Reservation( + private Reservation( Long id, Member member, PayMethod payMethod, diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/exception/InvalidRequestException.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/exception/InvalidRequestException.java deleted file mode 100644 index 1991b6f6..00000000 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/exception/InvalidRequestException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.fc.shimpyo_be.domain.reservation.exception; - -import com.fc.shimpyo_be.global.exception.ApplicationException; -import com.fc.shimpyo_be.global.exception.ErrorCode; - -public class InvalidRequestException extends ApplicationException { - - public InvalidRequestException(ErrorCode errorCode) { - super(errorCode); - } -} diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/exception/UnavailableRoomsException.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/exception/PreoccupyNotAvailableException.java similarity index 64% rename from src/main/java/com/fc/shimpyo_be/domain/reservation/exception/UnavailableRoomsException.java rename to src/main/java/com/fc/shimpyo_be/domain/reservation/exception/PreoccupyNotAvailableException.java index 48d949f0..693bd869 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/exception/UnavailableRoomsException.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/exception/PreoccupyNotAvailableException.java @@ -1,21 +1,21 @@ package com.fc.shimpyo_be.domain.reservation.exception; -import com.fc.shimpyo_be.domain.reservation.dto.response.ValidationResultResponseDto; +import com.fc.shimpyo_be.domain.reservation.dto.response.ValidatePreoccupyResultResponseDto; import com.fc.shimpyo_be.global.exception.ErrorCode; import lombok.Getter; @Getter -public class UnavailableRoomsException extends RuntimeException { +public class PreoccupyNotAvailableException extends RuntimeException { private final ErrorCode errorCode; - private ValidationResultResponseDto data; + private ValidatePreoccupyResultResponseDto data; - public UnavailableRoomsException() { + public PreoccupyNotAvailableException() { super(ErrorCode.UNAVAILABLE_ROOMS.getSimpleMessage()); this.errorCode = ErrorCode.UNAVAILABLE_ROOMS; } - public UnavailableRoomsException(ValidationResultResponseDto data) { + public PreoccupyNotAvailableException(ValidatePreoccupyResultResponseDto data) { super(ErrorCode.UNAVAILABLE_ROOMS.getSimpleMessage()); this.errorCode = ErrorCode.UNAVAILABLE_ROOMS; this.data = data; diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/exception/ReservationRestControllerAdvice.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/exception/ReservationRestControllerAdvice.java index d5ce25c7..6a65a598 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/exception/ReservationRestControllerAdvice.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/exception/ReservationRestControllerAdvice.java @@ -1,6 +1,7 @@ package com.fc.shimpyo_be.domain.reservation.exception; -import com.fc.shimpyo_be.domain.reservation.dto.response.ValidationResultResponseDto; +import com.fc.shimpyo_be.domain.reservation.dto.response.ValidateReservationResultResponseDto; +import com.fc.shimpyo_be.domain.reservation.dto.response.ValidatePreoccupyResultResponseDto; import com.fc.shimpyo_be.global.common.ResponseDto; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -11,9 +12,17 @@ @RestControllerAdvice public class ReservationRestControllerAdvice { - @ExceptionHandler(UnavailableRoomsException.class) - public ResponseEntity> unavailableRoomsException( - UnavailableRoomsException e) { + @ExceptionHandler(ReserveNotAvailableException.class) + public ResponseEntity> unavailableToReserveException( + ReserveNotAvailableException e) { + return ResponseEntity + .status(e.getErrorCode().getHttpStatus()) + .body(ResponseDto.res(e.getErrorCode().getHttpStatus(), e.getData(), e.getMessage())); + } + + @ExceptionHandler(PreoccupyNotAvailableException.class) + public ResponseEntity> unavailableToPreoccupyException( + PreoccupyNotAvailableException e) { return ResponseEntity .status(e.getErrorCode().getHttpStatus()) .body(ResponseDto.res(e.getErrorCode().getHttpStatus(), e.getData(), e.getMessage())); diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/exception/ReserveNotAvailableException.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/exception/ReserveNotAvailableException.java new file mode 100644 index 00000000..44af0c37 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/exception/ReserveNotAvailableException.java @@ -0,0 +1,23 @@ +package com.fc.shimpyo_be.domain.reservation.exception; + +import com.fc.shimpyo_be.domain.reservation.dto.response.ValidateReservationResultResponseDto; +import com.fc.shimpyo_be.global.exception.ErrorCode; +import lombok.Getter; + +@Getter +public class ReserveNotAvailableException extends RuntimeException { + + private final ErrorCode errorCode; + private ValidateReservationResultResponseDto data; + + public ReserveNotAvailableException() { + super(ErrorCode.RESERVATION_VALIDATION_FAIL.getSimpleMessage()); + this.errorCode = ErrorCode.RESERVATION_VALIDATION_FAIL; + } + + public ReserveNotAvailableException(ValidateReservationResultResponseDto data) { + super(ErrorCode.RESERVATION_VALIDATION_FAIL.getSimpleMessage()); + this.errorCode = ErrorCode.RESERVATION_VALIDATION_FAIL; + this.data = data; + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/facade/PreoccupyRoomsLockFacade.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/facade/PreoccupyRoomsLockFacade.java index a8aa2828..81892b32 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/facade/PreoccupyRoomsLockFacade.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/facade/PreoccupyRoomsLockFacade.java @@ -2,9 +2,9 @@ import com.fc.shimpyo_be.domain.reservation.dto.CheckAvailableRoomsResultDto; import com.fc.shimpyo_be.domain.reservation.dto.request.PreoccupyRoomsRequestDto; -import com.fc.shimpyo_be.domain.reservation.dto.response.ValidationResultResponseDto; +import com.fc.shimpyo_be.domain.reservation.dto.response.ValidatePreoccupyResultResponseDto; import com.fc.shimpyo_be.domain.reservation.exception.RedissonLockFailException; -import com.fc.shimpyo_be.domain.reservation.exception.UnavailableRoomsException; +import com.fc.shimpyo_be.domain.reservation.exception.PreoccupyNotAvailableException; import com.fc.shimpyo_be.domain.reservation.service.PreoccupyRoomsService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -23,8 +23,7 @@ public class PreoccupyRoomsLockFacade { private final RedissonClient redissonClient; private final PreoccupyRoomsService preoccupyRoomsService; - - public void checkAvailableAndPreoccupy(Long memberId, PreoccupyRoomsRequestDto request) { + public ValidatePreoccupyResultResponseDto checkAvailableAndPreoccupy(Long memberId, PreoccupyRoomsRequestDto request) { RLock lock = redissonClient.getLock("check-preoccupy"); String currentWorker = Thread.currentThread().getName(); @@ -37,15 +36,22 @@ public void checkAvailableAndPreoccupy(Long memberId, PreoccupyRoomsRequestDto r } CheckAvailableRoomsResultDto resultDto = preoccupyRoomsService.checkAvailable(memberId, request); + if(!resultDto.isAvailable()) { - log.info("[{}][check available rooms result] isAvailable = {}, unavailableIds = {}", currentWorker, false, resultDto.unavailableIds()); - throw new UnavailableRoomsException( - new ValidationResultResponseDto(false, resultDto.unavailableIds()) + throw new PreoccupyNotAvailableException( + ValidatePreoccupyResultResponseDto.builder() + .isAvailable(false) + .roomResults(resultDto.roomResults()) + .build() ); } - log.info("[{}][check available rooms result] isAvailable = {}, unavailableIds = {}", currentWorker, true, resultDto.unavailableIds()); - preoccupyRoomsService.preoccupy(request, resultDto.recordMap()); + preoccupyRoomsService.preoccupy(resultDto); + + return ValidatePreoccupyResultResponseDto.builder() + .isAvailable(true) + .roomResults(resultDto.roomResults()) + .build(); } catch (InterruptedException exception) { log.error("exception : {}, message : {}", exception.getClass().getSimpleName(), exception.getMessage()); diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/facade/ReservationLockFacade.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/facade/ReservationLockFacade.java index a9893ce0..27615b04 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/facade/ReservationLockFacade.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/facade/ReservationLockFacade.java @@ -1,10 +1,11 @@ package com.fc.shimpyo_be.domain.reservation.facade; +import com.fc.shimpyo_be.domain.reservation.dto.ValidateReservationResultDto; import com.fc.shimpyo_be.domain.reservation.dto.request.SaveReservationRequestDto; import com.fc.shimpyo_be.domain.reservation.dto.response.SaveReservationResponseDto; -import com.fc.shimpyo_be.domain.reservation.dto.response.ValidationResultResponseDto; +import com.fc.shimpyo_be.domain.reservation.dto.response.ValidateReservationResultResponseDto; import com.fc.shimpyo_be.domain.reservation.exception.RedissonLockFailException; -import com.fc.shimpyo_be.domain.reservation.exception.UnavailableRoomsException; +import com.fc.shimpyo_be.domain.reservation.exception.ReserveNotAvailableException; import com.fc.shimpyo_be.domain.reservation.service.ReservationService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -27,24 +28,25 @@ public SaveReservationResponseDto saveReservation(Long memberId, SaveReservation String currentWorker = Thread.currentThread().getName(); try { - boolean isLocked = lock.tryLock(2, 4, TimeUnit.SECONDS); + boolean isLocked = lock.tryLock(3, 5, TimeUnit.SECONDS); if(!isLocked) { log.error("[{}] 예약 lock 획득 실패", currentWorker); throw new RedissonLockFailException(); } - ValidationResultResponseDto resultDto = reservationService.validate(memberId, request.reservationProducts()); + ValidateReservationResultDto resultDto = reservationService.validate(memberId, request.reservationProducts()); if(!resultDto.isAvailable()) { - log.info("[{}][validate rooms result] isAvailable = {}, unavailableIds = {}", currentWorker, false, resultDto.unavailableIds()); - throw new UnavailableRoomsException( - new ValidationResultResponseDto(false, resultDto.unavailableIds()) + throw new ReserveNotAvailableException( + ValidateReservationResultResponseDto.builder() + .isAvailable(false) + .unavailableIds(resultDto.unavailableIds()) + .build() ); } - log.info("[{}][validate rooms result] isAvailable = {}, unavailableIds = {}", currentWorker, true, resultDto.unavailableIds()); - return reservationService.saveReservation(memberId, request); + return reservationService.saveReservation(memberId, request, resultDto.confirmMap()); } catch (InterruptedException exception) { log.error("exception : {}, message : {}", exception.getClass().getSimpleName(), exception.getMessage()); diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/service/PreoccupyRoomsService.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/service/PreoccupyRoomsService.java index ef26e6cb..9c584d94 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/service/PreoccupyRoomsService.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/service/PreoccupyRoomsService.java @@ -1,8 +1,10 @@ package com.fc.shimpyo_be.domain.reservation.service; import com.fc.shimpyo_be.domain.reservation.dto.CheckAvailableRoomsResultDto; -import com.fc.shimpyo_be.domain.reservation.dto.request.PreoccupyRoomsRequestDto; import com.fc.shimpyo_be.domain.reservation.dto.request.PreoccupyRoomItemRequestDto; +import com.fc.shimpyo_be.domain.reservation.dto.request.PreoccupyRoomsRequestDto; +import com.fc.shimpyo_be.domain.reservation.dto.response.ValidatePreoccupyRoomResponseDto; +import com.fc.shimpyo_be.domain.room.service.RoomService; import com.fc.shimpyo_be.global.util.DateTimeUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -11,6 +13,7 @@ import org.springframework.stereotype.Service; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.ZoneId; import java.util.*; @@ -19,63 +22,118 @@ @Service public class PreoccupyRoomsService { + private final RoomService roomService; private final RedisTemplate redisTemplate; - private static final String REDIS_KEY_FORMAT = "roomId:%d:%s"; + private static final String PREOCCUPY_REDIS_KEY_FORMAT = "roomId:%d:%s"; + private static final String CHECK_DUPLICATE_FORMAT = "%d:%s:%s"; public CheckAvailableRoomsResultDto checkAvailable(Long memberId, PreoccupyRoomsRequestDto request) { ValueOperations opsForValue = redisTemplate.opsForValue(); boolean isAvailable = true; - List unavailableIds = new LinkedList<>(); - Map> recordMap = new HashMap<>(); String memberIdValue = String.valueOf(memberId); + Map> preoccupyMap = new HashMap<>(); + Set checkSet = new HashSet<>(); + List roomResults = new ArrayList<>(); + for (PreoccupyRoomItemRequestDto room : request.rooms()) { - LocalDate targetDate = DateTimeUtil.toLocalDate(room.startDate()); + LocalDate startDate = DateTimeUtil.toLocalDate(room.startDate()); LocalDate endDate = DateTimeUtil.toLocalDate(room.endDate()); + Long cartId = room.cartId(); + Long roomCode = room.roomCode(); - while(targetDate.isBefore(endDate)) { + List roomIds = roomService.getRoomIdsByCode(roomCode); - String key = String.format(REDIS_KEY_FORMAT, room.roomId(), targetDate); - Object value = opsForValue.get(key); + boolean roomIdCheck = false; + for (Long roomId : roomIds) { - log.info("roomId: {}, targetDate: {}, value: {}", room.roomId(), targetDate, value); - if(Objects.nonNull(value)) { - isAvailable = false; - unavailableIds.add(room.roomId()); - break; + if(checkSet.contains(String.format(CHECK_DUPLICATE_FORMAT, roomId, startDate, endDate))) { + continue; + } + + boolean dateCheck = true; + LocalDate targetDate = startDate; + while(targetDate.isBefore(endDate)) { + + String key = String.format(PREOCCUPY_REDIS_KEY_FORMAT, roomId, targetDate); + Object value = opsForValue.get(key); + + if(Objects.nonNull(value)) { + dateCheck = false; + preoccupyMap.remove(roomId); + break; + } + + preoccupyMap + .computeIfAbsent(roomId, k -> new LinkedHashMap<>()) + .put(key, memberIdValue); + + targetDate = targetDate.plusDays(1); } - recordMap - .computeIfAbsent(room.roomId(), k -> new LinkedHashMap<>()) - .put(key, memberIdValue); + if(dateCheck) { + roomIdCheck = true; + + roomResults.add( + ValidatePreoccupyRoomResponseDto.builder() + .cartId(cartId) + .roomCode(roomCode) + .startDate(DateTimeUtil.toString(startDate)) + .endDate(DateTimeUtil.toString(endDate)) + .roomId(roomId) + .build() + ); + + checkSet.add(String.format(CHECK_DUPLICATE_FORMAT, roomId, startDate, endDate)); - targetDate = targetDate.plusDays(1); + break; + } + } + + if (!roomIdCheck) { + isAvailable = false; + + roomResults.add( + ValidatePreoccupyRoomResponseDto.builder() + .cartId(cartId) + .roomCode(roomCode) + .startDate(DateTimeUtil.toString(startDate)) + .endDate(DateTimeUtil.toString(endDate)) + .roomId(-1L) + .build() + ); } } - return new CheckAvailableRoomsResultDto(isAvailable, unavailableIds, recordMap); + return CheckAvailableRoomsResultDto.builder() + .isAvailable(isAvailable) + .roomResults(roomResults) + .preoccupyMap(preoccupyMap) + .build(); } - public void preoccupy(PreoccupyRoomsRequestDto request, Map> preoccupyMap) { + public void preoccupy(CheckAvailableRoomsResultDto resultDto) { ValueOperations opsForValue = redisTemplate.opsForValue(); - for (PreoccupyRoomItemRequestDto room : request.rooms()) { - Map map = preoccupyMap.get(room.roomId()); + Map> preoccupyMap = resultDto.preoccupyMap(); + + for (ValidatePreoccupyRoomResponseDto roomResult : resultDto.roomResults()) { + Map map = preoccupyMap.get(roomResult.roomId()); opsForValue.multiSet(map); - Date expireDate = convertLocalDateToDate(DateTimeUtil.toLocalDate(room.endDate())); + Date expireDate = convertLocalDateTimeToDate(LocalDateTime.now().plusMinutes(35)); for (String key : map.keySet()) { redisTemplate.expireAt(key, expireDate); } } } - private Date convertLocalDateToDate(LocalDate localDate) { + private Date convertLocalDateTimeToDate(LocalDateTime dateTime) { return Date.from( - localDate - .atStartOfDay(ZoneId.systemDefault()) + dateTime + .atZone(ZoneId.systemDefault()) .toInstant() ); } diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/service/ReservationService.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/service/ReservationService.java index d268c269..6e2c7371 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservation/service/ReservationService.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/service/ReservationService.java @@ -1,25 +1,22 @@ package com.fc.shimpyo_be.domain.reservation.service; +import com.fc.shimpyo_be.domain.cart.service.CartService; import com.fc.shimpyo_be.domain.member.entity.Member; -import com.fc.shimpyo_be.domain.member.exception.MemberNotFoundException; -import com.fc.shimpyo_be.domain.member.repository.MemberRepository; -import com.fc.shimpyo_be.domain.product.entity.Product; -import com.fc.shimpyo_be.domain.product.exception.RoomNotFoundException; +import com.fc.shimpyo_be.domain.member.service.MemberService; +import com.fc.shimpyo_be.domain.reservation.dto.ValidateReservationResultDto; import com.fc.shimpyo_be.domain.reservation.dto.request.ReleaseRoomItemRequestDto; import com.fc.shimpyo_be.domain.reservation.dto.request.ReleaseRoomsRequestDto; import com.fc.shimpyo_be.domain.reservation.dto.request.SaveReservationRequestDto; import com.fc.shimpyo_be.domain.reservation.dto.response.ReservationInfoResponseDto; import com.fc.shimpyo_be.domain.reservation.dto.response.SaveReservationResponseDto; -import com.fc.shimpyo_be.domain.reservation.dto.response.ValidationResultResponseDto; import com.fc.shimpyo_be.domain.reservation.entity.Reservation; -import com.fc.shimpyo_be.domain.reservation.exception.InvalidRequestException; import com.fc.shimpyo_be.domain.reservation.repository.ReservationRepository; +import com.fc.shimpyo_be.domain.reservation.util.mapper.ReservationMapper; import com.fc.shimpyo_be.domain.reservationproduct.dto.request.ReservationProductRequestDto; import com.fc.shimpyo_be.domain.reservationproduct.entity.ReservationProduct; import com.fc.shimpyo_be.domain.reservationproduct.repository.ReservationProductRepository; import com.fc.shimpyo_be.domain.room.entity.Room; -import com.fc.shimpyo_be.domain.room.repository.RoomRepository; -import com.fc.shimpyo_be.global.exception.ErrorCode; +import com.fc.shimpyo_be.domain.room.service.RoomService; import com.fc.shimpyo_be.global.util.DateTimeUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -32,11 +29,8 @@ import org.springframework.util.ObjectUtils; import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; +import java.time.ZoneId; +import java.util.*; @Slf4j @RequiredArgsConstructor @@ -45,24 +39,30 @@ public class ReservationService { private final ReservationRepository reservationRepository; private final ReservationProductRepository reservationProductRepository; - private final MemberRepository memberRepository; - private final RoomRepository roomRepository; + private final MemberService memberService; + private final RoomService roomService; + private final CartService cartService; private final RedisTemplate redisTemplate; private static final String REDIS_ROOM_KEY_FORMAT = "roomId:%d:%s"; @Transactional - public SaveReservationResponseDto saveReservation(Long memberId, SaveReservationRequestDto request) { - log.info("{} ::: {}", getClass().getSimpleName(), "saveReservation"); + public SaveReservationResponseDto saveReservation( + Long memberId, SaveReservationRequestDto request, Map> reservationMap + ) { // 회원 엔티티 조회 - Member member = memberRepository.findById(memberId) - .orElseThrow(MemberNotFoundException::new); + Member member = memberService.getMemberById(memberId); - // 객실 엔티티 조회 후, 예약 상품 리스트 생성 + // 객실 엔티티 조회 후, 예약 숙소 리스트 생성 List reservationProducts = new ArrayList<>(); for (ReservationProductRequestDto reservationProductDto : request.reservationProducts()) { - Room room = roomRepository.findById(reservationProductDto.roomId()) - .orElseThrow(RoomNotFoundException::new); + + Room room = roomService.getRoomById(reservationProductDto.roomId()); + + // 장바구니 아이템 삭제 + if (reservationProductDto.cartId() > 0) { + cartService.deleteCart(member.getId(), reservationProductDto.cartId()); + } reservationProducts.add( ReservationProduct.builder() @@ -74,95 +74,82 @@ public SaveReservationResponseDto saveReservation(Long memberId, SaveReservation .price(reservationProductDto.price()) .build() ); + + confirmReservationProduct(reservationMap.get(room.getId()), reservationProductDto.endDate()); } // 예약 저장 - Long reservationId - = reservationRepository.save( - Reservation.builder() - .member(member) - .reservationProducts(reservationProducts) - .payMethod(request.payMethod()) - .totalPrice(request.totalPrice()) - .build()).getId(); - - return new SaveReservationResponseDto(reservationId, request); + Reservation reservation = reservationRepository.save( + Reservation.builder() + .member(member) + .reservationProducts(reservationProducts) + .payMethod(request.payMethod()) + .totalPrice(request.totalPrice()) + .build() + ); + + return ReservationMapper.toSaveReservationResponseDto(reservation); } @Transactional(readOnly = true) public Page getReservationInfoList(Long memberId, Pageable pageable) { - log.info("{} ::: {}", getClass().getSimpleName(), "getReservationInfoList"); List reservationIds = reservationRepository.findIdsByMemberId(memberId); - DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm"); return reservationProductRepository .findAllInReservationIds(reservationIds, pageable) - .map( - reservationProduct -> { - Reservation reservation = reservationProduct.getReservation(); - Room room = reservationProduct.getRoom(); - Product product = room.getProduct(); - - return new ReservationInfoResponseDto( - reservation.getId(), - reservationProduct.getId(), - product.getId(), - product.getName(), - product.getThumbnail(), - product.getAddress(), - room.getId(), - room.getName(), - dateFormatter.format(reservationProduct.getStartDate()), - dateFormatter.format(reservationProduct.getEndDate()), - timeFormatter.format(room.getCheckIn()), - timeFormatter.format(room.getCheckOut()), - reservationProduct.getPrice(), - reservation.getPayMethod().name() - ); - } - ); + .map(ReservationMapper::toReservationInfoResponseDto); } - public ValidationResultResponseDto validate(Long memberId, List reservationProducts) { - log.info("{} ::: {}", getClass().getSimpleName(), "validate"); + public ValidateReservationResultDto validate(Long memberId, List reservationProducts) { ValueOperations opsForValue = redisTemplate.opsForValue(); boolean isValid = true; List invalidRoomIds = new ArrayList<>(); + Map> confirmMap = new HashMap<>(); + String memberIdValue = String.valueOf(memberId); + for (ReservationProductRequestDto reservationProduct : reservationProducts) { + LocalDate targetDate = DateTimeUtil.toLocalDate(reservationProduct.startDate()); LocalDate endDate = DateTimeUtil.toLocalDate(reservationProduct.endDate()); - + Long roomId = reservationProduct.roomId(); List keys = new LinkedList<>(); - while(targetDate.isBefore(endDate)) { - keys.add(String.format(REDIS_ROOM_KEY_FORMAT, reservationProduct.roomId(), targetDate)); + + while (targetDate.isBefore(endDate)) { + keys.add(String.format(REDIS_ROOM_KEY_FORMAT, roomId, targetDate)); + targetDate = targetDate.plusDays(1); } + confirmMap.put(roomId, keys); + List values = opsForValue.multiGet(keys); - String memberIdValue = String.valueOf(memberId); - if(ObjectUtils.isEmpty(values)) { - throw new InvalidRequestException(ErrorCode.INVALID_RESERVATION_REQUEST); + if (ObjectUtils.isEmpty(values)) { + isValid = false; + invalidRoomIds.add(roomId); + continue; } for (Object value : values) { - if(!memberIdValue.equals(value)) { + if (Objects.isNull(value) || !memberIdValue.equals(value)) { isValid = false; - invalidRoomIds.add(Long.valueOf((String) value)); + invalidRoomIds.add(roomId); break; } } } - return new ValidationResultResponseDto(isValid, invalidRoomIds); + return ValidateReservationResultDto.builder() + .isAvailable(isValid) + .unavailableIds(invalidRoomIds) + .confirmMap(confirmMap) + .build(); } public void releaseRooms(Long memberId, ReleaseRoomsRequestDto request) { - log.info("{} ::: {}", getClass().getSimpleName(), "releaseRooms"); String memberIdValue = String.valueOf(memberId); ValueOperations opsForValue = redisTemplate.opsForValue(); @@ -185,4 +172,20 @@ public void releaseRooms(Long memberId, ReleaseRoomsRequestDto request) { redisTemplate.delete(deleteKeys); } } + + private void confirmReservationProduct(List reservationKeys, String endDate) { + + Date expireDate = convertLocalDateToDate(DateTimeUtil.toLocalDate(endDate)); + for (String key : reservationKeys) { + redisTemplate.expireAt(key, expireDate); + } + } + + private Date convertLocalDateToDate(LocalDate localDate) { + return Date.from( + localDate + .atStartOfDay(ZoneId.systemDefault()) + .toInstant() + ); + } } diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservation/util/mapper/ReservationMapper.java b/src/main/java/com/fc/shimpyo_be/domain/reservation/util/mapper/ReservationMapper.java new file mode 100644 index 00000000..5ad17e62 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/reservation/util/mapper/ReservationMapper.java @@ -0,0 +1,75 @@ +package com.fc.shimpyo_be.domain.reservation.util.mapper; + +import com.fc.shimpyo_be.domain.product.entity.Product; +import com.fc.shimpyo_be.domain.reservation.dto.response.ReservationInfoResponseDto; +import com.fc.shimpyo_be.domain.reservation.dto.response.SaveReservationResponseDto; +import com.fc.shimpyo_be.domain.reservation.entity.Reservation; +import com.fc.shimpyo_be.domain.reservationproduct.dto.response.ReservationProductResponseDto; +import com.fc.shimpyo_be.domain.reservationproduct.entity.ReservationProduct; +import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.global.util.DateTimeUtil; + +import java.util.ArrayList; +import java.util.List; + +public class ReservationMapper { + + public static ReservationInfoResponseDto toReservationInfoResponseDto(ReservationProduct reservationProduct) { + Reservation reservation = reservationProduct.getReservation(); + Room room = reservationProduct.getRoom(); + Product product = room.getProduct(); + + return ReservationInfoResponseDto.builder() + .reservationId(reservation.getId()) + .reservationProductId(reservationProduct.getId()) + .productId(product.getId()) + .productName(product.getName()) + .productImageUrl(product.getThumbnail()) + .productAddress(product.getAddress().getAddress()) + .productDetailAddress(product.getAddress().getDetailAddress()) + .roomId(room.getId()) + .roomName(room.getName()) + .startDate(DateTimeUtil.toString(reservationProduct.getStartDate())) + .endDate(DateTimeUtil.toString(reservationProduct.getEndDate())) + .checkIn(DateTimeUtil.toString(room.getCheckIn())) + .checkOut(DateTimeUtil.toString(room.getCheckOut())) + .price(reservationProduct.getPrice()) + .payMethod(reservation.getPayMethod().name()) + .createdAt(DateTimeUtil.toString(reservation.getCreatedAt())) + .build(); + } + + public static SaveReservationResponseDto toSaveReservationResponseDto(Reservation reservation) { + List reservationProductDtos = new ArrayList<>(); + for (ReservationProduct reservationProduct : reservation.getReservationProducts()) { + + Room room = reservationProduct.getRoom(); + Product product = room.getProduct(); + + reservationProductDtos.add( + ReservationProductResponseDto.builder() + .productName(product.getName()) + .roomId(room.getId()) + .roomName(room.getName()) + .standard(room.getStandard()) + .capacity(room.getCapacity()) + .startDate(DateTimeUtil.toString(reservationProduct.getStartDate())) + .endDate(DateTimeUtil.toString(reservationProduct.getEndDate())) + .checkIn(DateTimeUtil.toString(room.getCheckIn())) + .checkOut(DateTimeUtil.toString(room.getCheckOut())) + .visitorName(reservationProduct.getVisitorName()) + .visitorPhone(reservationProduct.getVisitorPhone()) + .price(reservationProduct.getPrice()) + .build() + ); + } + + return SaveReservationResponseDto.builder() + .reservationId(reservation.getId()) + .reservationProducts(reservationProductDtos) + .payMethod(reservation.getPayMethod()) + .totalPrice(reservation.getTotalPrice()) + .createdAt(DateTimeUtil.toString(reservation.getCreatedAt())) + .build(); + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/constant/ReservationProductValidationConstants.java b/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/constant/ReservationProductValidationConstants.java new file mode 100644 index 00000000..b7b29653 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/constant/ReservationProductValidationConstants.java @@ -0,0 +1,18 @@ +package com.fc.shimpyo_be.domain.reservationproduct.constant; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ReservationProductValidationConstants { + + // validation constraint value + public static final String DATE_REGEX = "^\\d{4}-\\d{2}-\\d{2}$"; + public static final String PHONE_NUMBER_REGEX = "^01(?:0|1|[6-9])-\\d{4}-\\d{4}$"; + public static final int PRICE_MIN_VALUE = 0; + + // validation message + public static final String DATE_PATTERN_MESSAGE = "올바른 날짜 형식이 아닙니다.(yyyy-MM-dd 형식으로 입력하세요.)"; + public static final String PHONE_NUMBER_PATTERN_MESSAGE = "올바른 휴대전화 번호를 입력하세요.('-' 포함)"; + public static final String PRICE_MIN_MESSAGE = "객실 이용 금액은 0원 이상부터 가능합니다."; +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/controller/ReservationProductRestController.java b/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/controller/ReservationProductRestController.java index eca49c73..51bd865d 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/controller/ReservationProductRestController.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/controller/ReservationProductRestController.java @@ -23,12 +23,11 @@ public class ReservationProductRestController { @DeleteMapping("/{id}") public ResponseEntity> cancel(@PathVariable Long id) { - log.info("[api][DELETE] /api/reservation-products"); reservationProductService.cancel(id, securityUtil.getCurrentMemberId()); return ResponseEntity .status(HttpStatus.OK) - .body(ResponseDto.res(HttpStatus.OK, "예약 상품이 정상적으로 취소 처리되었습니다.")); + .body(ResponseDto.res(HttpStatus.OK, "예약 숙소이 정상적으로 취소 처리되었습니다.")); } } diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/dto/request/ReservationProductRequestDto.java b/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/dto/request/ReservationProductRequestDto.java index dce3fe8a..cf73318a 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/dto/request/ReservationProductRequestDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/dto/request/ReservationProductRequestDto.java @@ -4,29 +4,26 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; +import lombok.Builder; +import static com.fc.shimpyo_be.domain.reservationproduct.constant.ReservationProductValidationConstants.*; + +@Builder public record ReservationProductRequestDto( @NotNull - Long roomId, - @NotBlank - String productName, - @NotBlank - String roomName, - @NotNull - Integer standard, + Long cartId, @NotNull - Integer max, - @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "올바른 날짜 형식이 아닙니다.(yyyy-MM-dd 형식으로 입력하세요.)") + Long roomId, + @Pattern(regexp = DATE_REGEX, message = DATE_PATTERN_MESSAGE) String startDate, - @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "올바른 날짜 형식이 아닙니다.(yyyy-MM-dd 형식으로 입력하세요.)") + @Pattern(regexp = DATE_REGEX, message = DATE_PATTERN_MESSAGE) String endDate, - @Pattern(regexp = "^\\d{2}:\\d{2}$", message = "올바른 시간 형식이 아닙니다.(HH:mm 형식으로 입력하세요.)") - String checkIn, - @Pattern(regexp = "^\\d{2}:\\d{2}$", message = "올바른 시간 형식이 아닙니다.(HH:mm 형식으로 입력하세요.)") - String checkOut, + @NotBlank String visitorName, + @NotBlank + @Pattern(regexp = PHONE_NUMBER_REGEX, message = PHONE_NUMBER_PATTERN_MESSAGE) String visitorPhone, - @Min(value = 0, message = "객실 이용 금액은 음수일 수 없습니다.") + @Min(value = PRICE_MIN_VALUE, message = PRICE_MIN_MESSAGE) Integer price ) { } diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/dto/response/ReservationProductResponseDto.java b/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/dto/response/ReservationProductResponseDto.java index 7452a5ff..41861df5 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/dto/response/ReservationProductResponseDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/dto/response/ReservationProductResponseDto.java @@ -1,13 +1,14 @@ package com.fc.shimpyo_be.domain.reservationproduct.dto.response; -import com.fc.shimpyo_be.domain.reservationproduct.dto.request.ReservationProductRequestDto; +import lombok.Builder; +@Builder public record ReservationProductResponseDto( - Long roomId, String productName, + Long roomId, String roomName, Integer standard, - Integer max, + Integer capacity, String startDate, String endDate, String checkIn, @@ -16,20 +17,4 @@ public record ReservationProductResponseDto( String visitorPhone, Integer price ) { - public ReservationProductResponseDto(ReservationProductRequestDto requestDto) { - this( - requestDto.roomId(), - requestDto.productName(), - requestDto.roomName(), - requestDto.standard(), - requestDto.max(), - requestDto.startDate(), - requestDto.endDate(), - requestDto.checkIn(), - requestDto.checkOut(), - requestDto.visitorName(), - requestDto.visitorPhone(), - requestDto.price() - ); - } } diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/entity/ReservationProduct.java b/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/entity/ReservationProduct.java index bcad45e7..8f399b97 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/entity/ReservationProduct.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/entity/ReservationProduct.java @@ -8,6 +8,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; import java.time.LocalDate; import java.time.LocalDateTime; @@ -19,24 +20,32 @@ public class ReservationProduct extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment(value = "예약 숙소 식별자") private Long id; + @Comment(value = "예약 주문 식별자") @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "reservation_id") private Reservation reservation; + @Comment(value = "예약 객실 식별자") @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "room_id") private Room room; + @Comment(value = "총 이용 금액") @Column(nullable = false) private int price; + @Comment(value = "숙박 시작일") @Column(nullable = false) private LocalDate startDate; + @Comment(value = "숙박 마지막일") @Column(nullable = false) private LocalDate endDate; + @Comment(value = "방문자명") private String visitorName; + @Comment(value = "방문자 전화번호") private String visitorPhone; @Builder - public ReservationProduct( + private ReservationProduct( Long id, Reservation reservation, Room room, diff --git a/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/service/ReservationProductService.java b/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/service/ReservationProductService.java index e5127b21..df1daefb 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/service/ReservationProductService.java +++ b/src/main/java/com/fc/shimpyo_be/domain/reservationproduct/service/ReservationProductService.java @@ -19,7 +19,6 @@ public class ReservationProductService { @Transactional public void cancel(Long id, Long memberId) { - log.info("{} ::: {}", getClass().getSimpleName(), "cancel"); ReservationProduct reservationProduct = reservationProductRepository.findByIdWithReservation(id) .orElseThrow(ReservationProductNotFoundException::new); diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/constant/RoomValidationConstants.java b/src/main/java/com/fc/shimpyo_be/domain/room/constant/RoomValidationConstants.java new file mode 100644 index 00000000..2807d7b9 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/room/constant/RoomValidationConstants.java @@ -0,0 +1,17 @@ +package com.fc.shimpyo_be.domain.room.constant; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class RoomValidationConstants { + + // validation constraint value + public static final int ROOM_REQ_MIN_SIZE = 1; + public static final int ROOM_REQ_MAX_SIZE = 3; + public static final long ROOMID_MIN_VALUE = 1; + + // validation message + public static final String ROOM_REQ_SIZE_MESSAGE = "최소 1개, 최대 3개의 객실 식별자 정보가 필요합니다."; + public static final String ROOMID_MIN_MESSAGE = "객실 식별자는 최소 1 이상이어야 합니다."; +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/controller/RoomRestController.java b/src/main/java/com/fc/shimpyo_be/domain/room/controller/RoomRestController.java new file mode 100644 index 00000000..9e26d2a8 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/room/controller/RoomRestController.java @@ -0,0 +1,49 @@ +package com.fc.shimpyo_be.domain.room.controller; + +import com.fc.shimpyo_be.domain.room.dto.response.RoomListWithProductInfoResponseDto; +import com.fc.shimpyo_be.domain.room.service.RoomService; +import com.fc.shimpyo_be.global.common.ResponseDto; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static com.fc.shimpyo_be.domain.room.constant.RoomValidationConstants.*; + +@Slf4j +@RequiredArgsConstructor +@Validated +@RequestMapping("/api/rooms") +@RestController +public class RoomRestController { + + private final RoomService roomService; + + @GetMapping + public ResponseEntity> getRoomsWithProductInfo( + @RequestParam @Size(min = ROOM_REQ_MIN_SIZE, max = ROOM_REQ_MAX_SIZE, message = ROOM_REQ_SIZE_MESSAGE) + List<@Min(value = ROOMID_MIN_VALUE, message = ROOMID_MIN_MESSAGE) Long> roomIds + ) { + + return ResponseEntity + .status(HttpStatus.OK) + .body( + ResponseDto.res( + HttpStatus.OK, + new RoomListWithProductInfoResponseDto( + roomService.getRoomsWithProductInfo(roomIds) + ), + "숙소 정보를 포함한 객실 정보 리스트가 정상적으로 조회되었습니다." + ) + ); + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/dto/response/RoomListWithProductInfoResponseDto.java b/src/main/java/com/fc/shimpyo_be/domain/room/dto/response/RoomListWithProductInfoResponseDto.java new file mode 100644 index 00000000..467e9774 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/room/dto/response/RoomListWithProductInfoResponseDto.java @@ -0,0 +1,11 @@ +package com.fc.shimpyo_be.domain.room.dto.response; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record RoomListWithProductInfoResponseDto( + List rooms +) { +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/dto/response/RoomOptionResponse.java b/src/main/java/com/fc/shimpyo_be/domain/room/dto/response/RoomOptionResponse.java new file mode 100644 index 00000000..90e94feb --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/room/dto/response/RoomOptionResponse.java @@ -0,0 +1,53 @@ +package com.fc.shimpyo_be.domain.room.dto.response; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +public record RoomOptionResponse( + + boolean bathFacility, + boolean bath, + boolean homeTheater, + boolean airCondition, + boolean tv, + boolean pc, + boolean cable, + boolean internet, + boolean refrigerator, + boolean toiletries, + boolean sofa, + boolean cooking, + boolean table, + boolean hairDryer + +) { + + @Builder + public RoomOptionResponse(boolean bathFacility, boolean bath, boolean homeTheater, + boolean airCondition, boolean tv, boolean pc, boolean cable, boolean internet, + boolean refrigerator, boolean toiletries, boolean sofa, boolean cooking, boolean table, + boolean hairDryer) { + this.bathFacility = bathFacility; + this.bath = bath; + this.homeTheater = homeTheater; + this.airCondition = airCondition; + this.tv = tv; + this.pc = pc; + this.cable = cable; + this.internet = internet; + this.refrigerator = refrigerator; + this.toiletries = toiletries; + this.sofa = sofa; + this.cooking = cooking; + this.table = table; + this.hairDryer = hairDryer; + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/dto/response/RoomResponse.java b/src/main/java/com/fc/shimpyo_be/domain/room/dto/response/RoomResponse.java index ab027eea..7a0a51f1 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/room/dto/response/RoomResponse.java +++ b/src/main/java/com/fc/shimpyo_be/domain/room/dto/response/RoomResponse.java @@ -1,12 +1,15 @@ package com.fc.shimpyo_be.domain.room.dto.response; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import lombok.Builder; import lombok.Getter; @Getter public class RoomResponse { - private final Long roomId; + private final Long roomCode; private final String roomName; private final Long price; private final String description; @@ -14,24 +17,53 @@ public class RoomResponse { private final Long capacity; private final String checkIn; private final String checkOut; - private Boolean reserved; + private final RoomOptionResponse roomOptionResponse; + private final List roomImages; + private Long remaining; @Builder - public RoomResponse(Long roomId, String roomName, Long price, String description, Long standard, - Long capacity, String checkIn, String checkOut, Boolean reserved) { - this.roomId = roomId; + private RoomResponse(Long roomCode, String roomName, Long price, String description, + Long standard, + Long capacity, String checkIn, String checkOut, Long remaining, + RoomOptionResponse roomOptionResponse, + List roomImages) { + this.roomCode = roomCode; this.roomName = roomName; this.price = price; - this.reserved = reserved; + this.remaining = remaining; this.description = description; this.standard = standard; this.capacity = capacity; this.checkIn = checkIn; this.checkOut = checkOut; + this.roomOptionResponse = roomOptionResponse; + if (roomImages == null) { + this.roomImages = new ArrayList<>(); + } else { + this.roomImages = roomImages; + } } - public void setReserved() { - reserved = true; + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + RoomResponse otherRoomResponse = (RoomResponse) obj; + return Objects.equals(roomCode, otherRoomResponse.roomCode); + } + + @Override + public int hashCode() { + return Objects.hash(roomCode); + } + + public void setRemaining(long remaining) { + this.remaining = remaining; } } diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/dto/response/RoomWithProductResponseDto.java b/src/main/java/com/fc/shimpyo_be/domain/room/dto/response/RoomWithProductResponseDto.java new file mode 100644 index 00000000..bcf2ad7d --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/room/dto/response/RoomWithProductResponseDto.java @@ -0,0 +1,20 @@ +package com.fc.shimpyo_be.domain.room.dto.response; + +import lombok.Builder; + +@Builder +public record RoomWithProductResponseDto( + Long productId, + String productName, + String productThumbnail, + String productAddress, + String productDetailAddress, + Long roomId, + String roomName, + Integer standard, + Integer capacity, + String checkIn, + String checkOut, + Long price +) { +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/entity/Room.java b/src/main/java/com/fc/shimpyo_be/domain/room/entity/Room.java index 0f79a02c..6056e7a2 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/room/entity/Room.java +++ b/src/main/java/com/fc/shimpyo_be/domain/room/entity/Room.java @@ -1,6 +1,7 @@ package com.fc.shimpyo_be.domain.room.entity; import com.fc.shimpyo_be.domain.product.entity.Product; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -9,11 +10,18 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; + import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -22,39 +30,61 @@ public class Room { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment("객실 식별자") private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id") + @Comment("숙소 식별자") private Product product; - @Column(length = 30) - private String name; @Column(nullable = false) + @Comment("객실 코드") + private long code; + @Column(nullable = false) + @Comment("객실 이름") + private String name; + @Column(columnDefinition = "TEXT", nullable = false) + @Comment("객실 설명") private String description; @Column(columnDefinition = "TINYINT") + @Comment("객실 기준인원") private int standard; @Column(columnDefinition = "TINYINT") + @Comment("객실 최대인원") private int capacity; @Column(columnDefinition = "TIME") + @Comment("객실 체크인 시간") private LocalTime checkIn; @Column(columnDefinition = "TIME") + @Comment("객실 체크아웃 시간") private LocalTime checkOut; - @Column(nullable = false) - private int price; + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @Comment("객실 가격") + private RoomPrice price; + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @Comment("객실 옵션 식별자") + private RoomOption roomOption; + @OneToMany(mappedBy = "room", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @Comment("객실 이미지 식별자") + private List roomImages = new ArrayList<>(); @Builder - public Room( + private Room( Long id, Product product, + long code, String name, String description, int standard, int capacity, - int price, LocalTime checkIn, - LocalTime checkOut + LocalTime checkOut, + RoomPrice price, + RoomOption roomOption, + List roomImages ) { this.id = id; this.product = product; + this.code = code; this.name = name; this.description = description; this.standard = standard; @@ -62,5 +92,7 @@ public Room( this.checkIn = checkIn; this.checkOut = checkOut; this.price = price; + this.roomOption = roomOption; + this.roomImages = roomImages; } } diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/entity/RoomImage.java b/src/main/java/com/fc/shimpyo_be/domain/room/entity/RoomImage.java new file mode 100644 index 00000000..509317fa --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/room/entity/RoomImage.java @@ -0,0 +1,45 @@ +package com.fc.shimpyo_be.domain.room.entity; + +import com.fc.shimpyo_be.domain.product.entity.Product; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class RoomImage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment("객실 이미지 식별자") + private Long id; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false, name = "room_id") + @Comment("객실 식별자") + private Room room; + @Column(nullable = false) + @Comment("객실 이미지 URL") + private String photoUrl; + @Column(nullable = false) + @Comment("객실 이미지 설명") + private String description; + + @Builder + private RoomImage(Long id, Room room, String photoUrl, String description) { + this.id = id; + this.room = room; + this.photoUrl = photoUrl; + this.description = description; + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/entity/RoomOption.java b/src/main/java/com/fc/shimpyo_be/domain/room/entity/RoomOption.java new file mode 100644 index 00000000..976d718a --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/room/entity/RoomOption.java @@ -0,0 +1,87 @@ +package com.fc.shimpyo_be.domain.room.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class RoomOption { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment("객실 옵션 식별자") + private Long id; + @Column(nullable = false) + @Comment("목욕 시설 여부") + private boolean bathFacility; + @Column(nullable = false) + @Comment("욕조 여부") + private boolean bath; + @Column(nullable = false) + @Comment("홈시어터 여부") + private boolean homeTheater; + @Column(nullable = false) + @Comment("에어컨 여부") + private boolean airCondition; + @Column(nullable = false) + @Comment("TV 여부") + private boolean tv; + @Column(nullable = false) + @Comment("PC 여부") + private boolean pc; + @Column(nullable = false) + @Comment("케이블 설치 여부") + private boolean cable; + @Column(nullable = false) + @Comment("인터넷 여부") + private boolean internet; + @Column(nullable = false) + @Comment("냉장고 여부") + private boolean refrigerator; + @Column(nullable = false) + @Comment("세면도구 여부") + private boolean toiletries; + @Column(nullable = false) + @Comment("소파 여부") + private boolean sofa; + @Column(nullable = false) + @Comment("취사용품 여부") + private boolean cooking; + @Column(nullable = false) + @Comment("테이블 여부") + private boolean diningTable; + @Column(nullable = false) + @Comment("드라이기 여부") + private boolean hairDryer; + + @Builder + private RoomOption(Long id, boolean bathFacility, boolean bath, boolean homeTheater, + boolean airCondition, boolean tv, boolean pc, boolean cable, boolean internet, + boolean refrigerator, boolean toiletries, boolean sofa, boolean cooking, boolean diningTable, + boolean hairDryer) { + this.id = id; + this.bathFacility = bathFacility; + this.bath = bath; + this.homeTheater = homeTheater; + this.airCondition = airCondition; + this.tv = tv; + this.pc = pc; + this.cable = cable; + this.internet = internet; + this.refrigerator = refrigerator; + this.toiletries = toiletries; + this.sofa = sofa; + this.cooking = cooking; + this.diningTable = diningTable; + this.hairDryer = hairDryer; + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/entity/RoomPrice.java b/src/main/java/com/fc/shimpyo_be/domain/room/entity/RoomPrice.java new file mode 100644 index 00000000..461dcb2e --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/room/entity/RoomPrice.java @@ -0,0 +1,45 @@ +package com.fc.shimpyo_be.domain.room.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class RoomPrice { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment("객실 가격 식별자") + private Long id; + @Column(nullable = false) + @Comment("비수기 주중 최소 가격") + private int offWeekDaysMinFee; + @Column(nullable = false) + @Comment("비수기 주말 최소 가격") + private int offWeekendMinFee; + @Column(nullable = false) + @Comment("성수기 주중 최소 가격") + private int peakWeekDaysMinFee; + @Column(nullable = false) + @Comment("성수기 주말 최소 가격") + private int peakWeekendMinFee; + + @Builder + private RoomPrice(Long id, int offWeekDaysMinFee, int offWeekendMinFee, int peakWeekDaysMinFee, + int peakWeekendMinFee) { + this.id = id; + this.offWeekDaysMinFee = offWeekDaysMinFee; + this.offWeekendMinFee = offWeekendMinFee; + this.peakWeekDaysMinFee = peakWeekDaysMinFee; + this.peakWeekendMinFee = peakWeekendMinFee; + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/exception/RoomNotFoundException.java b/src/main/java/com/fc/shimpyo_be/domain/room/exception/RoomNotFoundException.java similarity index 83% rename from src/main/java/com/fc/shimpyo_be/domain/product/exception/RoomNotFoundException.java rename to src/main/java/com/fc/shimpyo_be/domain/room/exception/RoomNotFoundException.java index 6afb1ae5..02d5d673 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/product/exception/RoomNotFoundException.java +++ b/src/main/java/com/fc/shimpyo_be/domain/room/exception/RoomNotFoundException.java @@ -1,4 +1,4 @@ -package com.fc.shimpyo_be.domain.product.exception; +package com.fc.shimpyo_be.domain.room.exception; import com.fc.shimpyo_be.global.exception.ApplicationException; import com.fc.shimpyo_be.global.exception.ErrorCode; diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/repository/RoomImageRepository.java b/src/main/java/com/fc/shimpyo_be/domain/room/repository/RoomImageRepository.java new file mode 100644 index 00000000..be9b263d --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/room/repository/RoomImageRepository.java @@ -0,0 +1,8 @@ +package com.fc.shimpyo_be.domain.room.repository; + +import com.fc.shimpyo_be.domain.room.entity.RoomImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RoomImageRepository extends JpaRepository { + +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/repository/RoomRepository.java b/src/main/java/com/fc/shimpyo_be/domain/room/repository/RoomRepository.java index b0478fdd..d1c283d3 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/room/repository/RoomRepository.java +++ b/src/main/java/com/fc/shimpyo_be/domain/room/repository/RoomRepository.java @@ -2,7 +2,16 @@ import com.fc.shimpyo_be.domain.room.entity.Room; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; -public interface RoomRepository extends JpaRepository { +import java.util.List; +public interface RoomRepository + extends JpaRepository, RoomRepositoryCustom { + + List findByCode(long code); + + @Query("select r.id from Room r where r.code = :code") + List findIdsByCode(@Param("code") Long code); } diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/repository/RoomRepositoryCustom.java b/src/main/java/com/fc/shimpyo_be/domain/room/repository/RoomRepositoryCustom.java new file mode 100644 index 00000000..0a68eacd --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/room/repository/RoomRepositoryCustom.java @@ -0,0 +1,10 @@ +package com.fc.shimpyo_be.domain.room.repository; + +import com.fc.shimpyo_be.domain.room.entity.Room; + +import java.util.List; + +public interface RoomRepositoryCustom { + + List findAllInIdsWithProductAndPrice(List roomIds); +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/repository/RoomRepositoryImpl.java b/src/main/java/com/fc/shimpyo_be/domain/room/repository/RoomRepositoryImpl.java new file mode 100644 index 00000000..9c988291 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/room/repository/RoomRepositoryImpl.java @@ -0,0 +1,26 @@ +package com.fc.shimpyo_be.domain.room.repository; + +import com.fc.shimpyo_be.domain.room.entity.Room; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +import static com.fc.shimpyo_be.domain.product.entity.QProduct.product; +import static com.fc.shimpyo_be.domain.room.entity.QRoom.room; +import static com.fc.shimpyo_be.domain.room.entity.QRoomPrice.roomPrice; + +@RequiredArgsConstructor +public class RoomRepositoryImpl implements RoomRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List findAllInIdsWithProductAndPrice(List roomIds) { + return jpaQueryFactory.selectFrom(room) + .join(room.product, product).fetchJoin() + .join(room.price, roomPrice).fetchJoin() + .where(room.id.in(roomIds)) + .fetch(); + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/service/RoomService.java b/src/main/java/com/fc/shimpyo_be/domain/room/service/RoomService.java new file mode 100644 index 00000000..87cb8142 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/room/service/RoomService.java @@ -0,0 +1,42 @@ +package com.fc.shimpyo_be.domain.room.service; + +import com.fc.shimpyo_be.domain.room.exception.RoomNotFoundException; +import com.fc.shimpyo_be.domain.room.dto.response.RoomWithProductResponseDto; +import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.domain.room.repository.RoomRepository; +import com.fc.shimpyo_be.domain.room.util.RoomMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Service +public class RoomService { + + private final RoomRepository roomRepository; + + @Transactional(readOnly = true) + public List getRoomsWithProductInfo(List roomIds) { + + return roomRepository.findAllInIdsWithProductAndPrice(roomIds) + .stream() + .map(RoomMapper::toRoomWithProductResponse) + .toList(); + } + + @Transactional(readOnly = true) + public List getRoomIdsByCode(Long roomCode) { + + return roomRepository.findIdsByCode(roomCode); + } + + @Transactional(readOnly = true) + public Room getRoomById(Long id) { + return roomRepository.findById(id) + .orElseThrow(RoomNotFoundException::new); + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/room/util/RoomMapper.java b/src/main/java/com/fc/shimpyo_be/domain/room/util/RoomMapper.java index b0c6e36c..19d3fdf3 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/room/util/RoomMapper.java +++ b/src/main/java/com/fc/shimpyo_be/domain/room/util/RoomMapper.java @@ -1,21 +1,74 @@ package com.fc.shimpyo_be.domain.room.util; +import com.fc.shimpyo_be.domain.product.entity.Address; +import com.fc.shimpyo_be.domain.product.entity.Product; +import com.fc.shimpyo_be.domain.room.dto.response.RoomOptionResponse; import com.fc.shimpyo_be.domain.room.dto.response.RoomResponse; +import com.fc.shimpyo_be.domain.room.dto.response.RoomWithProductResponseDto; import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.domain.room.entity.RoomImage; +import com.fc.shimpyo_be.domain.room.entity.RoomOption; +import com.fc.shimpyo_be.global.util.DateTimeUtil; +import com.fc.shimpyo_be.global.util.PricePickerByDateUtil; public class RoomMapper { - public static RoomResponse from(Room room) { + public static RoomResponse toRoomResponse(Room room) { + + long price = PricePickerByDateUtil.getPrice(room); + price = price == 0 ? 100000 : price; + return RoomResponse.builder() - .roomId(room.getId()) + .roomCode(room.getCode()) .roomName(room.getName()) - .price((long) (room.getPrice())) + .price(price) .standard((long) (room.getStandard())) .capacity((long) room.getCapacity()) .description(room.getDescription()) - .reserved(false) .checkIn(room.getCheckIn().toString()) .checkOut(room.getCheckOut().toString()) + .roomOptionResponse(toRoomOptionResponse(room.getRoomOption())) + .roomImages(room.getRoomImages().stream().map(RoomImage::getPhotoUrl).toList()) + .remaining(0L) + .build(); + } + + private static RoomOptionResponse toRoomOptionResponse(RoomOption roomOption) { + return RoomOptionResponse.builder() + .airCondition(roomOption.isAirCondition()) + .pc(roomOption.isPc()) + .bath(roomOption.isBath()) + .bathFacility(roomOption.isBathFacility()) + .cooking(roomOption.isCooking()) + .tv(roomOption.isTv()) + .cable(roomOption.isCable()) + .hairDryer(roomOption.isHairDryer()) + .sofa(roomOption.isSofa()) + .table(roomOption.isDiningTable()) + .toiletries(roomOption.isToiletries()) + .homeTheater(roomOption.isHomeTheater()) + .internet(roomOption.isInternet()) + .refrigerator(roomOption.isRefrigerator()) + .build(); + } + + public static RoomWithProductResponseDto toRoomWithProductResponse(Room room) { + Product product = room.getProduct(); + Address productAddress = product.getAddress(); + + return RoomWithProductResponseDto.builder() + .productId(product.getId()) + .productName(product.getName()) + .productThumbnail(product.getThumbnail()) + .productAddress(productAddress.getAddress()) + .productDetailAddress(productAddress.getDetailAddress()) + .roomId(room.getId()) + .roomName(room.getName()) + .standard(room.getStandard()) + .capacity(room.getCapacity()) + .checkIn(DateTimeUtil.toString(room.getCheckIn())) + .checkOut(DateTimeUtil.toString(room.getCheckOut())) + .price(PricePickerByDateUtil.getPrice(room)) .build(); } } diff --git a/src/main/java/com/fc/shimpyo_be/domain/star/constant/StarValidationConstants.java b/src/main/java/com/fc/shimpyo_be/domain/star/constant/StarValidationConstants.java new file mode 100644 index 00000000..9813d0a4 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/star/constant/StarValidationConstants.java @@ -0,0 +1,20 @@ +package com.fc.shimpyo_be.domain.star.constant; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class StarValidationConstants { + + // validation constraint value + public static final String SCORE_DECIMAL_MIN_VALUE = "0.0"; + public static final String SCORE_DECIMAL_MAX_VALUE = "5.0"; + public static final int SCORE_DIGITS_INTEGER_VALUE = 1; + public static final int SCORE_DIGITS_FRACTION_VALUE = 1; + + // validation message + public static final String STAR_PRODUCTID_NOTNULL_MESSAGE = "별점 등록 대상 숙소 아이디는 필수값입니다."; + public static final String SCORE_DECIMAL_MIN_MESSAGE = "별점은 최소 0.0점 미만일 수 없습니다."; + public static final String SCORE_DECIMAL_MAX_MESSAGE = "별점은 최대 5.0점을 초과할 수 없습니다."; + public static final String SCORE_DIGITS_MESSAGE = "별점은 정수 1자리, 소수점 1자리까지만 가능합니다."; +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/star/controller/StarRestController.java b/src/main/java/com/fc/shimpyo_be/domain/star/controller/StarRestController.java index 641be44b..3f97c659 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/star/controller/StarRestController.java +++ b/src/main/java/com/fc/shimpyo_be/domain/star/controller/StarRestController.java @@ -10,10 +10,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @Slf4j @RequiredArgsConstructor @@ -26,7 +23,6 @@ public class StarRestController { @PostMapping public ResponseEntity> register(@Valid @RequestBody StarRegisterRequestDto request) { - log.info("[api][POST] /api/stars"); return ResponseEntity .status(HttpStatus.CREATED) diff --git a/src/main/java/com/fc/shimpyo_be/domain/star/dto/request/StarRegisterRequestDto.java b/src/main/java/com/fc/shimpyo_be/domain/star/dto/request/StarRegisterRequestDto.java index f66c7699..07c3e74c 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/star/dto/request/StarRegisterRequestDto.java +++ b/src/main/java/com/fc/shimpyo_be/domain/star/dto/request/StarRegisterRequestDto.java @@ -5,14 +5,17 @@ import jakarta.validation.constraints.Digits; import jakarta.validation.constraints.NotNull; +import static com.fc.shimpyo_be.domain.star.constant.StarValidationConstants.*; + public record StarRegisterRequestDto( @NotNull Long reservationProductId, - @NotNull(message = "별점 등록 대상 숙소 아이디는 필수값입니다.") + @NotNull(message = STAR_PRODUCTID_NOTNULL_MESSAGE) Long productId, - @DecimalMax(value = "5.0", message = "별점은 최대 5.0점을 초과할 수 없습니다.") - @DecimalMin(value = "0.0", message = "별점은 최소 0.0점 미만일 수 없습니다.") - @Digits(integer = 1, fraction = 1, message = "별점은 정수 1자리, 소수점 1자리까지만 가능합니다.") + @DecimalMax(value = SCORE_DECIMAL_MAX_VALUE, message = SCORE_DECIMAL_MAX_MESSAGE) + @DecimalMin(value = SCORE_DECIMAL_MIN_VALUE, message = SCORE_DECIMAL_MIN_MESSAGE) + @Digits(integer = SCORE_DIGITS_INTEGER_VALUE, fraction = SCORE_DIGITS_FRACTION_VALUE, + message = SCORE_DIGITS_MESSAGE) float score ) { } diff --git a/src/main/java/com/fc/shimpyo_be/domain/star/entity/Star.java b/src/main/java/com/fc/shimpyo_be/domain/star/entity/Star.java index 7e357b38..a64ecbef 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/star/entity/Star.java +++ b/src/main/java/com/fc/shimpyo_be/domain/star/entity/Star.java @@ -14,6 +14,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -22,18 +23,22 @@ public class Star { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment(value = "별점 식별자") private Long id; + @Comment(value = "회원 식별자") @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; + @Comment(value = "별점 등록 숙소 식별자") @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id") private Product product; + @Comment(value = "별점 점수") @Column(nullable = false) private float score; @Builder - public Star(Long id, Member member, Product product, float score) { + private Star(Long id, Member member, Product product, float score) { this.id = id; this.member = member; this.product = product; diff --git a/src/main/java/com/fc/shimpyo_be/domain/star/exception/ExpiredRegisterDateException.java b/src/main/java/com/fc/shimpyo_be/domain/star/exception/ExpiredRegisterDateException.java new file mode 100644 index 00000000..ae8bcfee --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/domain/star/exception/ExpiredRegisterDateException.java @@ -0,0 +1,11 @@ +package com.fc.shimpyo_be.domain.star.exception; + +import com.fc.shimpyo_be.global.exception.ApplicationException; +import com.fc.shimpyo_be.global.exception.ErrorCode; + +public class ExpiredRegisterDateException extends ApplicationException { + + public ExpiredRegisterDateException() { + super(ErrorCode.EXPIRED_STAR_REGISTER_DATE); + } +} diff --git a/src/main/java/com/fc/shimpyo_be/domain/star/service/StarService.java b/src/main/java/com/fc/shimpyo_be/domain/star/service/StarService.java index 32d3742d..baeb4fff 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/star/service/StarService.java +++ b/src/main/java/com/fc/shimpyo_be/domain/star/service/StarService.java @@ -1,8 +1,7 @@ package com.fc.shimpyo_be.domain.star.service; import com.fc.shimpyo_be.domain.member.entity.Member; -import com.fc.shimpyo_be.domain.member.exception.MemberNotFoundException; -import com.fc.shimpyo_be.domain.member.repository.MemberRepository; +import com.fc.shimpyo_be.domain.member.service.MemberService; import com.fc.shimpyo_be.domain.product.entity.Product; import com.fc.shimpyo_be.domain.product.exception.ProductNotFoundException; import com.fc.shimpyo_be.domain.product.repository.ProductRepository; @@ -13,6 +12,7 @@ import com.fc.shimpyo_be.domain.star.dto.response.StarResponseDto; import com.fc.shimpyo_be.domain.star.entity.Star; import com.fc.shimpyo_be.domain.star.exception.CannotBeforeCheckOutException; +import com.fc.shimpyo_be.domain.star.exception.ExpiredRegisterDateException; import com.fc.shimpyo_be.domain.star.repository.StarRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -29,36 +29,27 @@ public class StarService { private final StarRepository starRepository; - private final MemberRepository memberRepository; + private final MemberService memberService; private final ProductRepository productRepository; private final ReservationProductRepository reservationProductRepository; @Transactional public StarResponseDto register(Long memberId, StarRegisterRequestDto request) { - log.debug("{} ::: {}", getClass().getSimpleName(), "register"); - // 회원 조회 - Member member = memberRepository.findById(memberId) - .orElseThrow(MemberNotFoundException::new); + Member member = memberService.getMemberById(memberId); - // 예약 상품 조회 ReservationProduct reservationProduct = reservationProductRepository.findByIdWithRoom(request.reservationProductId()) .orElseThrow(ReservationProductNotFoundException::new); - // 별점 등록 가능 기간인지 검증 validateRegisterDate(LocalDateTime.now(), reservationProduct.getEndDate(), reservationProduct.getRoom().getCheckOut()); - // 상품 조회 Product product = productRepository.findById(request.productId()) .orElseThrow(ProductNotFoundException::new); - // 별점 총 수 카운트 long total = starRepository.countByProduct(product); - // 별점 평균 업데이트 product.updateStarAvg(calculateStarAvg(product.getStarAvg(), total, request.score())); - // 별점 등록 return new StarResponseDto( starRepository.save( Star.builder() @@ -77,11 +68,19 @@ private float calculateStarAvg(float currentAvg, long total, float score) { } private void validateRegisterDate(LocalDateTime current, LocalDate endDate, LocalTime checkOut) { - LocalDateTime target = LocalDateTime.of(endDate, checkOut); + LocalDateTime startDateTime = LocalDateTime.of(endDate, checkOut); - if (current.isEqual(target) || current.isBefore(target)) { + if (current.isEqual(startDateTime) || current.isBefore(startDateTime)) { log.error("별점 등록은 체크아웃 이후에 가능합니다."); throw new CannotBeforeCheckOutException(); } + + LocalDateTime endDateTime = + LocalDateTime.of(endDate.plusDays(14), LocalTime.of(23, 59, 59)); + + if (current.isAfter(endDateTime)) { + log.error("별점 등록은 체크아웃 후 2주 이내에만 가능합니다."); + throw new ExpiredRegisterDateException(); + } } } diff --git a/src/main/java/com/fc/shimpyo_be/global/exception/ErrorCode.java b/src/main/java/com/fc/shimpyo_be/global/exception/ErrorCode.java index ff996420..ca992834 100644 --- a/src/main/java/com/fc/shimpyo_be/global/exception/ErrorCode.java +++ b/src/main/java/com/fc/shimpyo_be/global/exception/ErrorCode.java @@ -17,33 +17,38 @@ public enum ErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원 정보를 찾을 수 없습니다."), WRONG_PASSWORD(HttpStatus.UNAUTHORIZED, "비밀번호가 틀렸습니다."), - // 상품 - PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "상품 정보를 찾을 수 없습니다."), + // 숙소 + PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "숙소 정보를 찾을 수 없습니다."), ROOM_NOT_RESERVE(HttpStatus.FORBIDDEN, "예약 불가능한 방입니다."), ROON_NOT_FOUND(HttpStatus.NOT_FOUND, "방 정보를 찾을 수 없습니다."), // 예약 UNAVAILABLE_ROOMS(HttpStatus.BAD_REQUEST, "예약 불가능한 방이 존재합니다."), LOCK_FAIL(HttpStatus.BAD_REQUEST, "요청 완료에 실패했습니다. 재시도가 필요합니다."), - INVALID_RESERVATION_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 예약 요청 데이터입니다."), + RESERVATION_VALIDATION_FAIL(HttpStatus.BAD_REQUEST, "예약 선점 기한이 만료되었거나, 잘못된 예약 요청으로 예약이 불가합니다."), - // 예약 상품 - RESERVATION_PRODUCT_NOT_FOUND(HttpStatus.BAD_REQUEST, "예약 상품 정보를 찾을 수 없습니다."), - FORBIDDEN_CANCEL_RESERVATION_PRODUCT(HttpStatus.FORBIDDEN, "예약 상품을 취소할 권한이 없습니다."), + // 예약 숙소 + RESERVATION_PRODUCT_NOT_FOUND(HttpStatus.BAD_REQUEST, "예약 숙소 정보를 찾을 수 없습니다."), + FORBIDDEN_CANCEL_RESERVATION_PRODUCT(HttpStatus.FORBIDDEN, "예약 숙소을 취소할 권한이 없습니다."), // 장바구니 CART_NOT_FOUND(HttpStatus.NOT_FOUND, "장바구니 정보를 찾을 수 없습니다."), CART_NOT_DELETE(HttpStatus.FORBIDDEN, "해당 장바구니를 삭제할 권한이 없습니다"), // 별점 - REGISTER_BEFORE_CHECKOUT(HttpStatus.BAD_REQUEST, "별점 등록 가능 기간이 아닙니다."), + REGISTER_BEFORE_CHECKOUT(HttpStatus.BAD_REQUEST, "별점 등록은 체크아웃 이후에 가능합니다."), + EXPIRED_STAR_REGISTER_DATE(HttpStatus.BAD_REQUEST, "별점 등록 가능 기간이 만료되었습니다.(체크아웃 후 2주 이내)"), // Open API HTTP_CLIENT_CONNECTION_ERROR(HttpStatus.UNAUTHORIZED, "외부 API 연결에 실패했습니다."), OPEN_API_ERROR(HttpStatus.NOT_FOUND, "오픈 API에서 데이터를 불러오는데 실패했습니다."), - //Common - INVALID_DATE(HttpStatus.BAD_REQUEST,"잘못된 날짜 데이터입니다."); + // Common + INVALID_DATE(HttpStatus.BAD_REQUEST,"잘못된 날짜 데이터입니다."), + + // 즐겨찾기 + FAVORITE_ALREADY_REGISTER(HttpStatus.BAD_REQUEST, "이미 즐겨찾기에 등록한 숙소입니다."), + FAVORITE_NOT_FOUND(HttpStatus.NOT_FOUND, "즐겨찾기 정보를 찾을 수 없습니다."); private final HttpStatus httpStatus; private final String simpleMessage; diff --git a/src/main/java/com/fc/shimpyo_be/global/util/DateTimeUtil.java b/src/main/java/com/fc/shimpyo_be/global/util/DateTimeUtil.java index dfe0921c..5c1c9069 100644 --- a/src/main/java/com/fc/shimpyo_be/global/util/DateTimeUtil.java +++ b/src/main/java/com/fc/shimpyo_be/global/util/DateTimeUtil.java @@ -1,6 +1,7 @@ package com.fc.shimpyo_be.global.util; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; @@ -8,6 +9,7 @@ public class DateTimeUtil { public final static String LOCAL_DATE_PATTERN = "yyyy-MM-dd"; public final static String LOCAL_TIME_PATTERN = "HH:mm"; + public final static String LOCAL_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; public final static String LOCAL_DATE_REGEX_PATTERN = "\\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])"; @@ -24,6 +26,10 @@ public static String toString(LocalTime timeObject) { return timeObject.format(DateTimeFormatter.ofPattern(LOCAL_TIME_PATTERN)); } + public static String toString(LocalDateTime dateTimeObject) { + return dateTimeObject.format(DateTimeFormatter.ofPattern(LOCAL_DATETIME_PATTERN)); + } + public static LocalDate toLocalDate(String dateString) { return LocalDate.parse(dateString, DateTimeFormatter.ofPattern(LOCAL_DATE_PATTERN)); } diff --git a/src/main/java/com/fc/shimpyo_be/global/util/PricePickerByDateUtil.java b/src/main/java/com/fc/shimpyo_be/global/util/PricePickerByDateUtil.java new file mode 100644 index 00000000..4757d960 --- /dev/null +++ b/src/main/java/com/fc/shimpyo_be/global/util/PricePickerByDateUtil.java @@ -0,0 +1,51 @@ +package com.fc.shimpyo_be.global.util; + +import com.fc.shimpyo_be.domain.room.entity.Room; +import java.time.LocalDate; + +public class PricePickerByDateUtil { + + private final static LocalDate today = LocalDate.now(); + + + public static long getPrice(Room room) { + + boolean isPeakTime = isPeakTime(); + boolean isWeekend = isWeekend(); + if (isPeakTime && isWeekend) { + return room.getPrice().getPeakWeekendMinFee(); + } else if (isPeakTime && !isWeekend) { + return room.getPrice().getPeakWeekDaysMinFee(); + } else if (!isPeakTime && isWeekend) { + return room.getPrice().getOffWeekendMinFee(); + } else { + return room.getPrice().getOffWeekDaysMinFee(); + } + } + + + public static boolean isWeekend() { + boolean isWeekend = false; + + isWeekend = switch (today.getDayOfWeek().getValue()) { + case 6, 7 -> true; + default -> isWeekend; + }; + + return isWeekend; + + } + + public static boolean isPeakTime() { + boolean isPeakMonth = true; + + isPeakMonth = switch (today.getMonth()) { + //성수기 + case MARCH, APRIL, MAY, JUNE, SEPTEMBER -> false; + default -> isPeakMonth; + }; + + return isPeakMonth; + } + +} diff --git a/src/main/java/com/fc/shimpyo_be/global/util/SecurityUtil.java b/src/main/java/com/fc/shimpyo_be/global/util/SecurityUtil.java index 2f92fb1d..88624c91 100644 --- a/src/main/java/com/fc/shimpyo_be/global/util/SecurityUtil.java +++ b/src/main/java/com/fc/shimpyo_be/global/util/SecurityUtil.java @@ -15,4 +15,14 @@ public Long getCurrentMemberId() { } return Long.parseLong(authentication.getName()); } + + public Long getNullableCurrentMemberId() { + final Authentication authentication = SecurityContextHolder.getContext() + .getAuthentication(); + if (authentication == null || authentication.getName() == null || authentication.getName().equals("anonymousUser")) { + return null; + } + + return Long.parseLong(authentication.getName()); + } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml deleted file mode 100644 index 12e3355b..00000000 --- a/src/main/resources/application.yaml +++ /dev/null @@ -1,3 +0,0 @@ -spring: - profiles: - default: prod diff --git a/src/main/resources/image/cart-docs.png b/src/main/resources/image/cart-docs.png deleted file mode 100644 index 92d3c3b1..00000000 Binary files a/src/main/resources/image/cart-docs.png and /dev/null differ diff --git a/src/main/resources/image/docs-error.png b/src/main/resources/image/docs-error.png new file mode 100644 index 00000000..7b4465d7 Binary files /dev/null and b/src/main/resources/image/docs-error.png differ diff --git a/src/main/resources/image/docs/cart-docs.png b/src/main/resources/image/docs/cart-docs.png new file mode 100644 index 00000000..8bce70a1 Binary files /dev/null and b/src/main/resources/image/docs/cart-docs.png differ diff --git a/src/main/resources/image/docs/favorite-docs.png b/src/main/resources/image/docs/favorite-docs.png new file mode 100644 index 00000000..dd3f9974 Binary files /dev/null and b/src/main/resources/image/docs/favorite-docs.png differ diff --git a/src/main/resources/image/index-docs.png b/src/main/resources/image/docs/index-docs.png similarity index 100% rename from src/main/resources/image/index-docs.png rename to src/main/resources/image/docs/index-docs.png diff --git a/src/main/resources/image/docs/member-docs.png b/src/main/resources/image/docs/member-docs.png new file mode 100644 index 00000000..7f8508f7 Binary files /dev/null and b/src/main/resources/image/docs/member-docs.png differ diff --git a/src/main/resources/image/docs/product-docs.png b/src/main/resources/image/docs/product-docs.png new file mode 100644 index 00000000..c76c4ba0 Binary files /dev/null and b/src/main/resources/image/docs/product-docs.png differ diff --git a/src/main/resources/image/docs/reservation-docs.png b/src/main/resources/image/docs/reservation-docs.png new file mode 100644 index 00000000..4fbe06cc Binary files /dev/null and b/src/main/resources/image/docs/reservation-docs.png differ diff --git a/src/main/resources/image/docs/reservation-product-docs.png b/src/main/resources/image/docs/reservation-product-docs.png new file mode 100644 index 00000000..1b26250f Binary files /dev/null and b/src/main/resources/image/docs/reservation-product-docs.png differ diff --git a/src/main/resources/image/docs/room-docs.png b/src/main/resources/image/docs/room-docs.png new file mode 100644 index 00000000..db0832d7 Binary files /dev/null and b/src/main/resources/image/docs/room-docs.png differ diff --git a/src/main/resources/image/docs/star-docs.png b/src/main/resources/image/docs/star-docs.png new file mode 100644 index 00000000..08db15b6 Binary files /dev/null and b/src/main/resources/image/docs/star-docs.png differ diff --git a/src/main/resources/image/erd.png b/src/main/resources/image/erd.png index 8c04e67c..67056c54 100644 Binary files a/src/main/resources/image/erd.png and b/src/main/resources/image/erd.png differ diff --git a/src/main/resources/image/main-page.png b/src/main/resources/image/main-page.png new file mode 100644 index 00000000..13340c86 Binary files /dev/null and b/src/main/resources/image/main-page.png differ diff --git a/src/main/resources/image/member-docs.png b/src/main/resources/image/member-docs.png deleted file mode 100644 index 16fd7f5b..00000000 Binary files a/src/main/resources/image/member-docs.png and /dev/null differ diff --git a/src/main/resources/image/product-docs.png b/src/main/resources/image/product-docs.png deleted file mode 100644 index 17af6ec0..00000000 Binary files a/src/main/resources/image/product-docs.png and /dev/null differ diff --git a/src/main/resources/image/reservation-docs.png b/src/main/resources/image/reservation-docs.png deleted file mode 100644 index 1183fb08..00000000 Binary files a/src/main/resources/image/reservation-docs.png and /dev/null differ diff --git a/src/main/resources/image/reservation-product-docs.png b/src/main/resources/image/reservation-product-docs.png deleted file mode 100644 index 10351bf0..00000000 Binary files a/src/main/resources/image/reservation-product-docs.png and /dev/null differ diff --git a/src/main/resources/image/star-docs.png b/src/main/resources/image/star-docs.png deleted file mode 100644 index 92a3f61e..00000000 Binary files a/src/main/resources/image/star-docs.png and /dev/null differ diff --git a/src/main/resources/image/wrapper-error-1.png b/src/main/resources/image/wrapper-error-1.png new file mode 100644 index 00000000..ce58b621 Binary files /dev/null and b/src/main/resources/image/wrapper-error-1.png differ diff --git a/src/main/resources/image/wrapper-error-2.png b/src/main/resources/image/wrapper-error-2.png new file mode 100644 index 00000000..1fc49f8a Binary files /dev/null and b/src/main/resources/image/wrapper-error-2.png differ diff --git a/src/test/java/com/fc/shimpyo_be/config/DatabaseCleanUp.java b/src/test/java/com/fc/shimpyo_be/config/DatabaseCleanUp.java new file mode 100644 index 00000000..f48bf516 --- /dev/null +++ b/src/test/java/com/fc/shimpyo_be/config/DatabaseCleanUp.java @@ -0,0 +1,58 @@ +package com.fc.shimpyo_be.config; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.annotation.Profile; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.shaded.com.google.common.base.CaseFormat; + +import java.util.List; +import java.util.stream.Collectors; + + +@Profile("test") +public class DatabaseCleanUp implements InitializingBean { + + @PersistenceContext + private EntityManager entityManager; + + private List tableNames; + + @Override + public void afterPropertiesSet() throws Exception { + tableNames = entityManager.getMetamodel().getEntities().stream() + .filter(e -> e.getJavaType().getAnnotation(Entity.class) != null) + .map(e -> CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, e.getName())) + .collect(Collectors.toList()); + } + + @Transactional + public void cleanUp() { + entityManager.flush(); + + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); + for (String tableName : tableNames) { + entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate(); + entityManager + .createNativeQuery("ALTER TABLE " + tableName + " ALTER COLUMN id RESTART WITH 1") + .executeUpdate(); + } + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); + } + + @Transactional + public void cleanUp(String[] tables) { + entityManager.flush(); + + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); + for (String table : tables) { + entityManager.createNativeQuery("TRUNCATE TABLE " + table).executeUpdate(); + entityManager + .createNativeQuery("ALTER TABLE " + table + " ALTER COLUMN id RESTART WITH 1") + .executeUpdate(); + } + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); + } +} diff --git a/src/test/java/com/fc/shimpyo_be/config/TestDBCleanerConfig.java b/src/test/java/com/fc/shimpyo_be/config/TestDBCleanerConfig.java new file mode 100644 index 00000000..31925369 --- /dev/null +++ b/src/test/java/com/fc/shimpyo_be/config/TestDBCleanerConfig.java @@ -0,0 +1,13 @@ +package com.fc.shimpyo_be.config; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class TestDBCleanerConfig { + + @Bean + public DatabaseCleanUp databaseCleanUp() { + return new DatabaseCleanUp(); + } +} diff --git a/src/test/java/com/fc/shimpyo_be/domain/cart/docs/CartRestIntegrationDocsTest.java b/src/test/java/com/fc/shimpyo_be/domain/cart/docs/CartRestIntegrationDocsTest.java index dca45b9d..3f56e12e 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/cart/docs/CartRestIntegrationDocsTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/cart/docs/CartRestIntegrationDocsTest.java @@ -26,20 +26,22 @@ import com.fc.shimpyo_be.domain.product.repository.ProductRepository; import com.fc.shimpyo_be.domain.room.entity.Room; import com.fc.shimpyo_be.domain.room.repository.RoomRepository; +import com.fc.shimpyo_be.global.util.DateTimeUtil; import com.fc.shimpyo_be.global.util.SecurityUtil; +import java.time.LocalDate; import java.util.concurrent.ThreadLocalRandom; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.ResultActions; import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; -@AutoConfigureMockMvc + class CartRestIntegrationDocsTest extends RestDocsSupport { @MockBean @@ -68,19 +70,15 @@ class CartRestIntegrationDocsTest extends RestDocsSupport { @BeforeEach void initTest() { //given - member = memberRepository.save(memberRepository.save(Member.builder() + member = memberRepository.save(Member.builder() .email("wocjf" + ThreadLocalRandom.current().nextInt(100000) + "@naver.com") .photoUrl("hello,world.jpg").name("심재철").password("1234").authority(Authority.ROLE_USER) - .build())); + .build()); given(securityUtil.getCurrentMemberId()).willReturn(1L); product = productRepository.save(ProductFactory.createTestProduct()); - room = roomRepository.save(ProductFactory.createTestRoom(product)); - - for (int i = 0; i < 5; i++) { - Cart cart = cartRepository.save(CartFactory.createCartTest(room, member)); - } + room = roomRepository.save(ProductFactory.createTestRoom(product,0L)); objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); } @@ -89,17 +87,23 @@ void initTest() { @WithMockUser void getCarts() throws Exception { //given + for (int i = 0; i < 5; i++) { + Cart cart = cartRepository.save(CartFactory.createCartTest(room, member)); + } + + given(securityUtil.getCurrentMemberId()).willReturn(member.getId()); //when ResultActions resultActions = mockMvc.perform(get("/api/carts")); + //then resultActions.andExpect(status().isOk()).andDo(restDoc.document( responseFields(responseCommon()).and( fieldWithPath("data").type(JsonFieldType.ARRAY).description("응답 데이터"), fieldWithPath("data[].cartId").type(JsonFieldType.NUMBER).description("장바구니 아이디"), - fieldWithPath("data[].productId").type(JsonFieldType.NUMBER).description("상품 아이디"), - fieldWithPath("data[].productName").type(JsonFieldType.STRING).description("상품 이름"), - fieldWithPath("data[].image").type(JsonFieldType.STRING).description("상품 대표 이미지"), - fieldWithPath("data[].roomId").type(JsonFieldType.NUMBER).description("방 아이디"), + fieldWithPath("data[].productId").type(JsonFieldType.NUMBER).description("숙소 아이디"), + fieldWithPath("data[].productName").type(JsonFieldType.STRING).description("숙소 이름"), + fieldWithPath("data[].image").type(JsonFieldType.STRING).description("숙소 대표 이미지"), + fieldWithPath("data[].roomCode").type(JsonFieldType.NUMBER).description("방 코드"), fieldWithPath("data[].roomName").type(JsonFieldType.STRING).description("방 이름"), fieldWithPath("data[].price").type(JsonFieldType.NUMBER).description("총 가격"), fieldWithPath("data[].description").type(JsonFieldType.STRING).description("방 설명"), @@ -109,18 +113,21 @@ void getCarts() throws Exception { fieldWithPath("data[].endDate").type(JsonFieldType.STRING).description("숙박 종료일"), fieldWithPath("data[].checkIn").type(JsonFieldType.STRING).description("방 체크인 시간"), fieldWithPath("data[].checkOut").type(JsonFieldType.STRING) - .description("방 체크아웃 시간"), - fieldWithPath("data[].reserved").type(JsonFieldType.BOOLEAN) - .description("예약 가능 여부")))); - + .description("방 체크아웃 시간")))); } @Test @WithMockUser void addCart() throws Exception { //given - CartCreateRequest cartCreateRequest = CartCreateRequest.builder().startDate("2023-12-27") - .endDate("2023-12-30").price(100000L).roomId(room.getId()).build(); + LocalDate tommorrow = LocalDate.now().plusDays(1); + CartCreateRequest cartCreateRequest = CartCreateRequest.builder() + .startDate(DateTimeUtil.toString( + tommorrow)) + .endDate(DateTimeUtil.toString(tommorrow.plusDays(2))).price(100000L) + .roomCode(room.getCode()).build(); + + cartRepository.save(Cart.builder().roomCode(0L).price(10000L).member(member).startDate(tommorrow.plusDays(2)).endDate(tommorrow.plusDays(3)).build()); //when ResultActions resultActions = mockMvc.perform( post("/api/carts").content(objectMapper.writeValueAsString(cartCreateRequest)) @@ -128,17 +135,17 @@ void addCart() throws Exception { //then resultActions.andExpect(status().isOk()).andDo(restDoc.document( - requestFields(fieldWithPath("roomId").type(JsonFieldType.NUMBER).description("방 아이디"), + requestFields(fieldWithPath("roomCode").type(JsonFieldType.NUMBER).description("방 코드"), fieldWithPath("startDate").type(JsonFieldType.STRING).description("숙박 시작일"), fieldWithPath("endDate").type(JsonFieldType.STRING).description("숙박 종료일"), fieldWithPath("price").type(JsonFieldType.NUMBER).description("장바구니 가격")), responseFields(responseCommon()).and( fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), fieldWithPath("data.cartId").type(JsonFieldType.NUMBER).description("장바구니 아이디"), - fieldWithPath("data.productId").type(JsonFieldType.NUMBER).description("상품 아이디"), - fieldWithPath("data.productName").type(JsonFieldType.STRING).description("상품 이름"), - fieldWithPath("data.image").type(JsonFieldType.STRING).description("상품 대표 이미지"), - fieldWithPath("data.roomId").type(JsonFieldType.NUMBER).description("방 아이디"), + fieldWithPath("data.productId").type(JsonFieldType.NUMBER).description("숙소 아이디"), + fieldWithPath("data.productName").type(JsonFieldType.STRING).description("숙소 이름"), + fieldWithPath("data.image").type(JsonFieldType.STRING).description("숙소 대표 이미지"), + fieldWithPath("data.roomCode").type(JsonFieldType.NUMBER).description("방 코드"), fieldWithPath("data.roomName").type(JsonFieldType.STRING).description("방 이름"), fieldWithPath("data.price").type(JsonFieldType.NUMBER).description("총 가격"), fieldWithPath("data.description").type(JsonFieldType.STRING).description("방 설명"), @@ -149,9 +156,7 @@ void addCart() throws Exception { fieldWithPath("data.checkIn").type(JsonFieldType.STRING).description("방 체크인 시간"), fieldWithPath("data.checkOut").type(JsonFieldType.STRING).description("방 체크아웃 시간"), fieldWithPath(("data.checkOut")).type(JsonFieldType.STRING) - .description("방 체크아웃 시간"), - fieldWithPath("data.reserved").type(JsonFieldType.BOOLEAN) - .description("예약 가능 여부")))); + .description("방 체크아웃 시간")))); } @@ -168,7 +173,7 @@ void deleteCart() throws Exception { responseFields(responseCommon()).and( fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), fieldWithPath("data.cartId").type(JsonFieldType.NUMBER).description("장바구니 아이디"), - fieldWithPath("data.roomId").type(JsonFieldType.NUMBER).description("방 아이디"), + fieldWithPath("data.roomCode").type(JsonFieldType.NUMBER).description("방 코드"), fieldWithPath("data.startDate").type(JsonFieldType.STRING).description("숙박 시작일"), fieldWithPath("data.endDate").type(JsonFieldType.STRING).description("숙박 종료일")))); } diff --git a/src/test/java/com/fc/shimpyo_be/domain/cart/factory/CartFactory.java b/src/test/java/com/fc/shimpyo_be/domain/cart/factory/CartFactory.java index 907907ee..5310f3f4 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/cart/factory/CartFactory.java +++ b/src/test/java/com/fc/shimpyo_be/domain/cart/factory/CartFactory.java @@ -9,7 +9,7 @@ public class CartFactory { public static Cart createCartTest(Room room, Member member) { - return Cart.builder().room(room).member(member).price( + return Cart.builder().roomCode(room.getCode()).member(member).price( 100000L ).startDate(LocalDate.now().plusDays(1)) .endDate(LocalDate.now().plusDays(2)).build(); diff --git a/src/test/java/com/fc/shimpyo_be/domain/cart/unit/controller/CartRestControllerTest.java b/src/test/java/com/fc/shimpyo_be/domain/cart/unit/controller/CartRestControllerTest.java index 1b6fffe1..22bc47b3 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/cart/unit/controller/CartRestControllerTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/cart/unit/controller/CartRestControllerTest.java @@ -50,11 +50,11 @@ public class CartRestControllerTest extends AbstractContainersSupport { public static void initTest() { //given Product product = ProductFactory.createTestProduct(); - room = ProductFactory.createTestRoom(product); + room = ProductFactory.createTestRoom(product,0l); member = Member.builder().email("wocjf0513@naver.com").photoUrl("hello,world.jpg") .name("심재철").password("1234").authority(Authority.ROLE_USER).build(); cart = CartFactory.createCartTest(room, member); - cartResponse = CartMapper.toCartResponse(cart); + cartResponse = CartMapper.toCartResponse(cart,room); cartResponses.add(cartResponse); } @@ -73,7 +73,7 @@ void successToGetCarts() { @Test void successToAddCart() { //given - CartCreateRequest cartCreateRequest = CartCreateRequest.builder().roomId(room.getId()).price(cartResponse.getPrice()) + CartCreateRequest cartCreateRequest = CartCreateRequest.builder().roomCode(room.getCode()).price(cartResponse.getPrice()) .startDate(cartResponse.getStartDate()).endDate(cartResponse.getEndDate()) .build(); diff --git a/src/test/java/com/fc/shimpyo_be/domain/cart/unit/service/CartRestServiceTest.java b/src/test/java/com/fc/shimpyo_be/domain/cart/unit/service/CartRestServiceTest.java index fad49302..545484c8 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/cart/unit/service/CartRestServiceTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/cart/unit/service/CartRestServiceTest.java @@ -2,6 +2,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doReturn; @@ -10,6 +12,7 @@ import com.fc.shimpyo_be.domain.cart.dto.response.CartResponse; import com.fc.shimpyo_be.domain.cart.entity.Cart; import com.fc.shimpyo_be.domain.cart.factory.CartFactory; +import com.fc.shimpyo_be.domain.cart.repository.CartCustomRepositoryImpl; import com.fc.shimpyo_be.domain.cart.repository.CartRepository; import com.fc.shimpyo_be.domain.cart.service.CartService; import com.fc.shimpyo_be.domain.cart.util.CartMapper; @@ -23,6 +26,7 @@ import com.fc.shimpyo_be.domain.room.repository.RoomRepository; import com.fc.shimpyo_be.global.util.DateTimeUtil; import com.fc.shimpyo_be.global.util.SecurityUtil; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -36,7 +40,6 @@ @ExtendWith(MockitoExtension.class) public class CartRestServiceTest { - private static CartResponse cartResponse; private static Room room; private static Member member; private static Cart cart; @@ -50,6 +53,9 @@ public class CartRestServiceTest { private RoomRepository roomRepository; @Mock private CartRepository cartRepository; + + @Mock + private CartCustomRepositoryImpl cartCustomRepository; @Mock private SecurityUtil securityUtil; @InjectMocks @@ -60,23 +66,22 @@ public class CartRestServiceTest { public static void initTest() { //given Product product = ProductFactory.createTestProduct(); - room = ProductFactory.createTestRoom(product); + room = ProductFactory.createTestRoom(product, 0l); member = Member.builder().id(1L).email("wocjf0513@naver.com").photoUrl("hello,world.jpg") .name("심재철").password("1234").authority(Authority.ROLE_USER).build(); cart = CartFactory.createCartTest(room, member); - cartResponse = CartMapper.toCartResponse(cart); } @Test void SuccessToGetCarts() { //given + List carts = new ArrayList<>(); carts.add(cart); - List expectedCartResponses = carts.stream().map(CartMapper::toCartResponse) + List expectedCartResponses = carts.stream() + .map(cartEntity -> CartMapper.toCartResponse(cartEntity, room)) .toList(); - doReturn(true).when(productService) - .isAvailableForReservation(cart.getId(), DateTimeUtil.toString(cart.getStartDate()), - DateTimeUtil.toString(cart.getEndDate())); + given(roomRepository.findByCode(cart.getRoomCode())).willReturn(List.of(room)); given(cartRepository.findByMemberId(1L)).willReturn(Optional.of(carts)); given(securityUtil.getCurrentMemberId()).willReturn(1L); //when @@ -90,17 +95,20 @@ void SuccessToGetCarts() { void SuccessToAddCart() { //given - CartCreateRequest cartCreateRequest = CartCreateRequest.builder().roomId(room.getId()) + CartCreateRequest cartCreateRequest = CartCreateRequest.builder().roomCode(room.getCode()) .price(100000L).startDate("2023-11-27").endDate("2023-11-28").build(); - doReturn(true).when(productService) - .isAvailableForReservation(cartCreateRequest.roomId(), cartCreateRequest.startDate(), - cartCreateRequest.endDate()); - Cart expectedCart = CartMapper.toCart(cartCreateRequest, room, member); - CartResponse expectedCartResponse = CartMapper.toCartResponse(expectedCart); + Cart expectedCart = CartMapper.toCart(cartCreateRequest, member); + CartResponse expectedCartResponse = CartMapper.toCartResponse(expectedCart, room); given(cartRepository.save(any())).willReturn(expectedCart); + given(cartCustomRepository.countByRoomCodeAndMemberIdContainsDate(any(), + anyLong())).willReturn(0L); given(securityUtil.getCurrentMemberId()).willReturn(member.getId()); - given(roomRepository.findById(room.getId())).willReturn(Optional.ofNullable(room)); + given(roomRepository.findByCode(cartCreateRequest.roomCode())).willReturn(List.of(room)); given(memberRepository.findById(member.getId())).willReturn(Optional.ofNullable(member)); + doReturn(1L).when(productService) + .countAvailableForReservationUsingRoomCode(anyLong(), + anyString(), + anyString()); //when CartResponse result = cartService.addCart(cartCreateRequest); //then diff --git a/src/test/java/com/fc/shimpyo_be/domain/favorite/docs/FavoriteRestControllerDocsTest.java b/src/test/java/com/fc/shimpyo_be/domain/favorite/docs/FavoriteRestControllerDocsTest.java new file mode 100644 index 00000000..1c99e2f0 --- /dev/null +++ b/src/test/java/com/fc/shimpyo_be/domain/favorite/docs/FavoriteRestControllerDocsTest.java @@ -0,0 +1,160 @@ +package com.fc.shimpyo_be.domain.favorite.docs; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fc.shimpyo_be.config.RestDocsSupport; +import com.fc.shimpyo_be.domain.favorite.dto.FavoriteResponseDto; +import com.fc.shimpyo_be.domain.favorite.dto.FavoritesResponseDto; +import com.fc.shimpyo_be.domain.favorite.service.FavoriteService; +import com.fc.shimpyo_be.domain.product.dto.response.ProductResponse; +import com.fc.shimpyo_be.domain.product.entity.Category; +import com.fc.shimpyo_be.global.util.SecurityUtil; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Pageable; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.security.test.context.support.WithMockUser; + +public class FavoriteRestControllerDocsTest extends RestDocsSupport { + + @MockBean + private FavoriteService favoriteService; + + @MockBean + SecurityUtil securityUtil; + + @Test + @DisplayName("register()은 즐겨찾기를 등록할 수 있다.") + @WithMockUser(roles = "USER") + void register() throws Exception { + // given + FavoriteResponseDto favoriteResponseDto = FavoriteResponseDto.builder() + .favoriteId(1L) + .memberId(1L) + .productId(1L) + .build(); + + given(securityUtil.getCurrentMemberId()).willReturn(1L); + given(favoriteService.register(any(Long.TYPE), any(Long.TYPE))) + .willReturn(favoriteResponseDto); + + // when then + mockMvc.perform(post("/api/favorites/{productId}", 1L)) + .andExpect(status().isCreated()) + .andDo(restDoc.document( + responseFields(responseCommon()).and( + fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), + fieldWithPath("data.favoriteId").type(JsonFieldType.NUMBER) + .description("즐겨찾기 식별자"), + fieldWithPath("data.memberId").type(JsonFieldType.NUMBER).description("회원 식별자"), + fieldWithPath("data.productId").type(JsonFieldType.NUMBER).description("숙소 식별자") + )) + ); + } + + + @Test + @DisplayName("getFavorites()은 즐겨찾기 목록을 조회할 수 있다.") + @WithMockUser(roles = "USER") + void getFavorites() throws Exception { + // given + FavoritesResponseDto favoritesResponseDto = FavoritesResponseDto.builder() + .pageCount(10) + .products(List.of(ProductResponse.builder() + .productId(1L) + .productName("OO 호텔") + .category(Category.TOURIST_HOTEL.getName()) + .address("서울시 강남구 OO로 000-000 상세주소") + .favorites(true) + .image( + "https://fastly.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI") + .starAvg(5F) + .price(95000L) + .capacity(4L) + .build())) + .build(); + + given(securityUtil.getCurrentMemberId()).willReturn(1L); + given(favoriteService.getFavorites(any(Long.TYPE), any(Pageable.class))) + .willReturn(favoritesResponseDto); + + // when then + mockMvc.perform(get("/api/favorites") + .queryParam("page", "0") + .queryParam("size", "10")) + .andExpect(status().isOk()) + .andDo(restDoc.document( + queryParameters(parameterWithName("page").optional().description("페이지 인덱스"), + parameterWithName("size").optional().description("페이지 사이즈") + ), + responseFields(responseCommon()).and( + fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), + fieldWithPath("data.pageCount").type(JsonFieldType.NUMBER) + .description("총 페이지 개수"), + fieldWithPath("data.products").type(JsonFieldType.ARRAY) + .description("숙소 응답 데이터 배열"), + fieldWithPath("data.products[].productId").type( + JsonFieldType.NUMBER) + .description("숙소 아이디"), + fieldWithPath("data.products[].category").type(JsonFieldType.STRING) + .description("숙소 카테고리(호텔, 모텔, 풀빌라, 펜션)"), + fieldWithPath("data.products[].address").type(JsonFieldType.STRING) + .description("숙소 주소"), + fieldWithPath("data.products[].productName").type( + JsonFieldType.STRING) + .description("숙소 이름"), + fieldWithPath("data.products[].favorites").type( + JsonFieldType.BOOLEAN) + .description("즐겨찾기"), + fieldWithPath("data.products[].starAvg").type(JsonFieldType.NUMBER) + .description("숙소 평점"), + fieldWithPath("data.products[].image").type(JsonFieldType.STRING) + .description("숙소 썸네일 이미지"), + fieldWithPath("data.products[].price").type(JsonFieldType.NUMBER) + .description("숙소 내 방 최저 가격"), + fieldWithPath("data.products[].capacity").type(JsonFieldType.NUMBER) + .description("최대 인원") + )) + ); + } + + @Test + @DisplayName("cancel은 즐겨찾기를 취소할 수 있다.") + @WithMockUser(roles = "USER") + void cancel() throws Exception { + // given + FavoriteResponseDto favoriteResponseDto = FavoriteResponseDto.builder() + .favoriteId(1L) + .memberId(1L) + .productId(1L) + .build(); + + given(securityUtil.getCurrentMemberId()).willReturn(1L); + given(favoriteService.delete(any(Long.TYPE), any(Long.TYPE))) + .willReturn(favoriteResponseDto); + + // when then + mockMvc.perform(delete("/api/favorites/{productId}", 1L)) + .andExpect(status().isOk()) + .andDo(restDoc.document( + responseFields(responseCommon()).and( + fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), + fieldWithPath("data.favoriteId").type(JsonFieldType.NUMBER) + .description("즐겨찾기 식별자"), + fieldWithPath("data.memberId").type(JsonFieldType.NUMBER).description("회원 식별자"), + fieldWithPath("data.productId").type(JsonFieldType.NUMBER).description("숙소 식별자") + )) + ); + } +} diff --git a/src/test/java/com/fc/shimpyo_be/domain/favorite/unit/controller/FavoriteRestControllerTest.java b/src/test/java/com/fc/shimpyo_be/domain/favorite/unit/controller/FavoriteRestControllerTest.java new file mode 100644 index 00000000..456fe806 --- /dev/null +++ b/src/test/java/com/fc/shimpyo_be/domain/favorite/unit/controller/FavoriteRestControllerTest.java @@ -0,0 +1,173 @@ +package com.fc.shimpyo_be.domain.favorite.unit.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fc.shimpyo_be.config.AbstractContainersSupport; +import com.fc.shimpyo_be.domain.favorite.dto.FavoriteResponseDto; +import com.fc.shimpyo_be.domain.favorite.dto.FavoritesResponseDto; +import com.fc.shimpyo_be.domain.favorite.service.FavoriteService; +import com.fc.shimpyo_be.domain.product.dto.response.ProductResponse; +import com.fc.shimpyo_be.domain.product.entity.Category; +import com.fc.shimpyo_be.global.util.SecurityUtil; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Pageable; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest +@AutoConfigureMockMvc +public class FavoriteRestControllerTest extends AbstractContainersSupport { + + @Autowired + private MockMvc mockMvc; + + @Autowired + WebApplicationContext context; + + @MockBean + FavoriteService favoriteService; + + @MockBean + SecurityUtil securityUtil; + + @BeforeEach + public void setup() { + mockMvc = MockMvcBuilders + .webAppContextSetup(this.context) + .apply(springSecurity()) + .build(); + } + + @Nested + @DisplayName("register()은") + class Context_register { + + @Test + @DisplayName("즐겨찾기를 등록할 수 있다.") + @WithMockUser(roles = "USER") + void _willSuccess() throws Exception { + // given + FavoriteResponseDto favoriteResponseDto = FavoriteResponseDto.builder() + .favoriteId(1L) + .memberId(1L) + .productId(1L) + .build(); + + given(securityUtil.getCurrentMemberId()).willReturn(1L); + given(favoriteService.register(any(Long.TYPE), any(Long.TYPE))) + .willReturn(favoriteResponseDto); + + // when then + mockMvc.perform(post("/api/favorites/{productId}", 1L)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.code").isNumber()) + .andExpect(jsonPath("$.message").isString()) + .andExpect(jsonPath("$.data").isMap()) + .andExpect(jsonPath("$.data.favoriteId").isNumber()) + .andExpect(jsonPath("$.data.memberId").isNumber()) + .andExpect(jsonPath("$.data.productId").isNumber()) + .andDo(print()); + } + } + + @Nested + @DisplayName("getFavorites()은") + class Context_getFavorites { + + @Test + @DisplayName("즐겨찾기 목록을 조회할 수 있다.") + @WithMockUser(roles = "USER") + void _willSuccess() throws Exception { + // given + FavoritesResponseDto favoritesResponseDto = FavoritesResponseDto.builder() + .pageCount(10) + .products(List.of(ProductResponse.builder() + .productId(1L) + .productName("OO 호텔") + .category(Category.TOURIST_HOTEL.getName()) + .address("서울시 강남구 OO로 000-000 상세주소") + .favorites(true) + .image( + "https://fastly.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI") + .starAvg(5F) + .price(95000L) + .capacity(4L) + .build())) + .build(); + + given(securityUtil.getCurrentMemberId()).willReturn(1L); + given(favoriteService.getFavorites(any(Long.TYPE), any(Pageable.class))) + .willReturn(favoritesResponseDto); + + // when then + mockMvc.perform(get("/api/favorites") + .queryParam("page", "0") + .queryParam("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").isNumber()) + .andExpect(jsonPath("$.message").isString()) + .andExpect(jsonPath("$.data").isMap()) + .andExpect(jsonPath("$.data.pageCount").isNumber()) + .andExpect(jsonPath("$.data.products[0].productId").isNumber()) + .andExpect(jsonPath("$.data.products[0].productName").isString()) + .andExpect(jsonPath("$.data.products[0].category").isString()) + .andExpect(jsonPath("$.data.products[0].address").isString()) + .andExpect(jsonPath("$.data.products[0].favorites").isBoolean()) + .andExpect(jsonPath("$.data.products[0].image").isString()) + .andExpect(jsonPath("$.data.products[0].starAvg").isNumber()) + .andExpect(jsonPath("$.data.products[0].price").isNumber()) + .andExpect(jsonPath("$.data.products[0].capacity").isNumber()) + .andDo(print()); + } + } + + @Nested + @DisplayName("cancel()은") + class Context_cancel { + + @Test + @DisplayName("즐겨찾기를 취소할 수 있다.") + @WithMockUser(roles = "USER") + void _willSuccess() throws Exception { + // given + FavoriteResponseDto favoriteResponseDto = FavoriteResponseDto.builder() + .favoriteId(1L) + .memberId(1L) + .productId(1L) + .build(); + + given(securityUtil.getCurrentMemberId()).willReturn(1L); + given(favoriteService.delete(any(Long.TYPE), any(Long.TYPE))) + .willReturn(favoriteResponseDto); + + // when then + mockMvc.perform(delete("/api/favorites/{productId}", 1L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").isNumber()) + .andExpect(jsonPath("$.message").isString()) + .andExpect(jsonPath("$.data").isMap()) + .andExpect(jsonPath("$.data.favoriteId").isNumber()) + .andExpect(jsonPath("$.data.memberId").isNumber()) + .andExpect(jsonPath("$.data.productId").isNumber()) + .andDo(print()); + } + } +} diff --git a/src/test/java/com/fc/shimpyo_be/domain/favorite/unit/repository/FavoriteRepositoryTest.java b/src/test/java/com/fc/shimpyo_be/domain/favorite/unit/repository/FavoriteRepositoryTest.java new file mode 100644 index 00000000..835eba1b --- /dev/null +++ b/src/test/java/com/fc/shimpyo_be/domain/favorite/unit/repository/FavoriteRepositoryTest.java @@ -0,0 +1,182 @@ +package com.fc.shimpyo_be.domain.favorite.unit.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fc.shimpyo_be.config.TestJpaConfig; +import com.fc.shimpyo_be.config.TestQuerydslConfig; +import com.fc.shimpyo_be.domain.favorite.entity.Favorite; +import com.fc.shimpyo_be.domain.favorite.repository.FavoriteRepository; +import com.fc.shimpyo_be.domain.member.entity.Authority; +import com.fc.shimpyo_be.domain.member.entity.Member; +import com.fc.shimpyo_be.domain.member.repository.MemberRepository; +import com.fc.shimpyo_be.domain.product.entity.Address; +import com.fc.shimpyo_be.domain.product.entity.Amenity; +import com.fc.shimpyo_be.domain.product.entity.Category; +import com.fc.shimpyo_be.domain.product.entity.Product; +import com.fc.shimpyo_be.domain.product.entity.ProductOption; +import com.fc.shimpyo_be.domain.product.repository.ProductRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.ArrayList; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +@DataJpaTest +@Import({TestJpaConfig.class, TestQuerydslConfig.class}) +@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) +public class FavoriteRepositoryTest { + + @Autowired + private FavoriteRepository favoriteRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private ProductRepository productRepository; + + @PersistenceContext + private EntityManager entityManager; + + @BeforeEach + public void reset() { + entityManager.flush(); + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); + favoriteRepository.deleteAll(); + memberRepository.deleteAll(); + productRepository.deleteAll(); + entityManager.createNativeQuery("TRUNCATE TABLE favorite").executeUpdate(); + entityManager.createNativeQuery("TRUNCATE TABLE member").executeUpdate(); + entityManager.createNativeQuery("TRUNCATE TABLE product").executeUpdate(); + entityManager + .createNativeQuery("ALTER TABLE favorite ALTER COLUMN `id` RESTART WITH 1") + .executeUpdate(); + entityManager + .createNativeQuery("ALTER TABLE member ALTER COLUMN `id` RESTART WITH 1") + .executeUpdate(); + entityManager + .createNativeQuery("ALTER TABLE product ALTER COLUMN `id` RESTART WITH 1") + .executeUpdate(); + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); + } + + private Member saveMember() { + Member member = Member.builder() + .email("test@mail.com") + .name("test") + .password("$10$ygrAExVYmFTkZn2d0.Pk3Ot5CNZwIBjZH5f.WW0AnUq4w4PtBi9Nm") + .photoUrl( + "https://fastly.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI") + .authority(Authority.ROLE_USER) + .build(); + return memberRepository.save(member); + } + + private Product saveProduct() { + Product product = Product.builder() + .id(1L) + .name("OO 호텔") + .address(Address.builder() + .address("서울시 강남구 OO로 000-000") + .detailAddress("상세주소") + .mapX(1.0) + .mapY(1.0) + .build()) + .thumbnail( + "https://fastly.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI") + .category(Category.TOURIST_HOTEL) + .starAvg(5) + .description("호텔입니다.") + .rooms(new ArrayList<>()) + .productOption(ProductOption.builder() + .cooking(false) + .parking(false) + .pickup(false) + .foodPlace("") + .infoCenter("000-0000-0000") + .build()) + .amenity(Amenity.builder() + .barbecue(false) + .beauty(false) + .beverage(false) + .bicycle(false) + .campfire(false) + .karaoke(false) + .publicBath(false) + .publicPc(false) + .sauna(false) + .seminar(false) + .sports(false) + .fitness(false) + .build()) + .build(); + return productRepository.save(product); + } + + private void saveFavorite(Member member, Product product) { + Favorite favorite = Favorite.builder() + .member(member) + .product(product) + .build(); + favoriteRepository.save(favorite); + } + + @Nested + @DisplayName("findByMemberAndProduct()는") + class Context_findByMemberAndProduct { + + @Test + @DisplayName("회원과 숙소로 즐겨찾기 정보를 조회할 수 있다.") + void _willSuccess() { + // given + Member member = saveMember(); + Product product = saveProduct(); + saveFavorite(member, product); + + // when + Optional result = favoriteRepository.findByMemberAndProduct(member, product); + + // then + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getId()).isNotNull(); + assertThat(result.get().getMember().getId()).isEqualTo(member.getId()); + assertThat(result.get().getProduct().getId()).isEqualTo(product.getId()); + } + } + + @Nested + @DisplayName("findAllByMemberId()는") + class Context_findAllByMemberId { + + @Test + @DisplayName("회원으로 즐겨찾기 정보 목록을 조회할 수 있다.") + void _willSuccess() { + // given + Member member = saveMember(); + Product product = saveProduct(); + saveFavorite(member, product); + Pageable pageable = org.springframework.data.domain.PageRequest.of(0, 10); + + // when + Page result = favoriteRepository.findAllByMemberId(member.getId(), pageable); + + // then + assertThat(result.isEmpty()).isFalse(); + assertThat(result.getTotalPages()).isEqualTo(1); + assertThat(result.get().toList().get(0).getId()).isNotNull(); + assertThat(result.get().toList().get(0).getMember().getId()).isEqualTo(member.getId()); + assertThat(result.get().toList().get(0).getProduct().getId()).isEqualTo( + product.getId()); + } + } +} diff --git a/src/test/java/com/fc/shimpyo_be/domain/favorite/unit/service/FavoriteServiceTest.java b/src/test/java/com/fc/shimpyo_be/domain/favorite/unit/service/FavoriteServiceTest.java new file mode 100644 index 00000000..355e5fe8 --- /dev/null +++ b/src/test/java/com/fc/shimpyo_be/domain/favorite/unit/service/FavoriteServiceTest.java @@ -0,0 +1,295 @@ +package com.fc.shimpyo_be.domain.favorite.unit.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.fc.shimpyo_be.domain.favorite.dto.FavoriteResponseDto; +import com.fc.shimpyo_be.domain.favorite.dto.FavoritesResponseDto; +import com.fc.shimpyo_be.domain.favorite.entity.Favorite; +import com.fc.shimpyo_be.domain.favorite.repository.FavoriteRepository; +import com.fc.shimpyo_be.domain.favorite.service.FavoriteService; +import com.fc.shimpyo_be.domain.member.entity.Authority; +import com.fc.shimpyo_be.domain.member.entity.Member; +import com.fc.shimpyo_be.domain.member.service.MemberService; +import com.fc.shimpyo_be.domain.product.entity.Address; +import com.fc.shimpyo_be.domain.product.entity.Amenity; +import com.fc.shimpyo_be.domain.product.entity.Category; +import com.fc.shimpyo_be.domain.product.entity.Product; +import com.fc.shimpyo_be.domain.product.entity.ProductOption; +import com.fc.shimpyo_be.domain.product.repository.ProductRepository; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@ExtendWith(MockitoExtension.class) +public class FavoriteServiceTest { + + @InjectMocks + private FavoriteService favoriteService; + + @Mock + private FavoriteRepository favoriteRepository; + + @Mock + private MemberService memberService; + + @Mock + private ProductRepository productRepository; + + @Nested + @DisplayName("register()은") + class Context_register { + + @Test + @DisplayName("즐겨찾기를 등록할 수 있다.") + void _willSuccess() { + // given + Member member = Member.builder() + .id(1L) + .email("test@mail.com") + .name("test") + .password("$10$ygrAExVYmFTkZn2d0.Pk3Ot5CNZwIBjZH5f.WW0AnUq4w4PtBi9Nm") + .photoUrl( + "https://fastly.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI") + .authority(Authority.ROLE_USER) + .build(); + Product product = Product.builder() + .id(1L) + .name("OO 호텔") + .address(Address.builder() + .address("서울시 강남구 OO로 000-000") + .detailAddress("상세주소") + .mapX(1.0) + .mapY(1.0) + .build()) + .thumbnail( + "https://fastly.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI") + .category(Category.TOURIST_HOTEL) + .starAvg(5) + .description("호텔입니다.") + .rooms(new ArrayList<>()) + .productOption(ProductOption.builder() + .cooking(false) + .parking(false) + .pickup(false) + .foodPlace("") + .infoCenter("000-0000-0000") + .build()) + .amenity(Amenity.builder() + .barbecue(false) + .beauty(false) + .beverage(false) + .bicycle(false) + .campfire(false) + .karaoke(false) + .publicBath(false) + .publicPc(false) + .sauna(false) + .seminar(false) + .sports(false) + .fitness(false) + .build()) + .build(); + Favorite favorite = Favorite.builder() + .id(1L) + .member(member) + .product(product) + .build(); + + given(memberService.getMemberById(any(Long.TYPE))).willReturn(member); + given(productRepository.findById(any(Long.TYPE))).willReturn(Optional.of(product)); + given(favoriteRepository.findByMemberAndProduct(any(Member.class), any(Product.class))) + .willReturn(Optional.empty()); + given(favoriteRepository.save(any(Favorite.class))).willReturn(favorite); + + // when + FavoriteResponseDto result = favoriteService.register(1L, 1L); + + // then + assertNotNull(result); + assertThat(result).extracting("favoriteId", "memberId", "productId") + .containsExactly(1L, 1L, 1L); + + verify(memberService, times(1)).getMemberById(any(Long.TYPE)); + verify(productRepository, times(1)).findById(any(Long.TYPE)); + verify(favoriteRepository, times(1)) + .findByMemberAndProduct(any(Member.class), any(Product.class)); + verify(favoriteRepository, times(1)).save(any(Favorite.class)); + } + } + + @Nested + @DisplayName("getFavorites()은") + class Context_getFavorites { + + @Test + @DisplayName("즐겨찾기 목록을 조회할 수 있다.") + void _willSuccess() { + // given + Member member = Member.builder() + .id(1L) + .email("test@mail.com") + .name("test") + .password("$10$ygrAExVYmFTkZn2d0.Pk3Ot5CNZwIBjZH5f.WW0AnUq4w4PtBi9Nm") + .photoUrl( + "https://fastly.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI") + .authority(Authority.ROLE_USER) + .build(); + Product product = Product.builder() + .id(1L) + .name("OO 호텔") + .address(Address.builder() + .address("서울시 강남구 OO로 000-000") + .detailAddress("상세주소") + .mapX(1.0) + .mapY(1.0) + .build()) + .thumbnail( + "https://fastly.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI") + .category(Category.TOURIST_HOTEL) + .starAvg(5) + .description("호텔입니다.") + .rooms(new ArrayList<>()) + .productOption(ProductOption.builder() + .cooking(false) + .parking(false) + .pickup(false) + .foodPlace("") + .infoCenter("000-0000-0000") + .build()) + .amenity(Amenity.builder() + .barbecue(false) + .beauty(false) + .beverage(false) + .bicycle(false) + .campfire(false) + .karaoke(false) + .publicBath(false) + .publicPc(false) + .sauna(false) + .seminar(false) + .sports(false) + .fitness(false) + .build()) + .build(); + Favorite favorite = Favorite.builder() + .id(1L) + .member(member) + .product(product) + .build(); + Pageable pageable = org.springframework.data.domain.PageRequest.of(0, 10); + + given(memberService.getMemberById(any(Long.TYPE))).willReturn(member); + given(favoriteRepository.findAllByMemberId(any(Long.class), any(Pageable.class))) + .willReturn(new PageImpl<>(List.of(favorite))); + + // when + FavoritesResponseDto result = favoriteService.getFavorites(1L, pageable); + + // then + assertNotNull(result); + + verify(memberService, times(1)).getMemberById(any(Long.TYPE)); + verify(favoriteRepository, times(1)).findAllByMemberId(any(Long.class), + any(Pageable.class)); + } + } + + @Nested + @DisplayName("delete()은") + class Context_delete { + + @Test + @DisplayName("즐겨찾기를 취소할 수 있다.") + void _willSuccess() { + // given + Member member = Member.builder() + .id(1L) + .email("test@mail.com") + .name("test") + .password("$10$ygrAExVYmFTkZn2d0.Pk3Ot5CNZwIBjZH5f.WW0AnUq4w4PtBi9Nm") + .photoUrl( + "https://fastly.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI") + .authority(Authority.ROLE_USER) + .build(); + Product product = Product.builder() + .id(1L) + .name("OO 호텔") + .address(Address.builder() + .address("서울시 강남구 OO로 000-000") + .detailAddress("상세주소") + .mapX(1.0) + .mapY(1.0) + .build()) + .thumbnail( + "https://fastly.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI") + .category(Category.TOURIST_HOTEL) + .starAvg(5) + .description("호텔입니다.") + .rooms(new ArrayList<>()) + .productOption(ProductOption.builder() + .cooking(false) + .parking(false) + .pickup(false) + .foodPlace("") + .infoCenter("000-0000-0000") + .build()) + .amenity(Amenity.builder() + .barbecue(false) + .beauty(false) + .beverage(false) + .bicycle(false) + .campfire(false) + .karaoke(false) + .publicBath(false) + .publicPc(false) + .sauna(false) + .seminar(false) + .sports(false) + .fitness(false) + .build()) + .build(); + Favorite favorite = Favorite.builder() + .id(1L) + .member(member) + .product(product) + .build(); + + given(memberService.getMemberById(any(Long.TYPE))).willReturn(member); + given(productRepository.findById(any(Long.TYPE))).willReturn(Optional.of(product)); + given(favoriteRepository.findByMemberAndProduct(any(Member.class), any(Product.class))) + .willReturn(Optional.of(favorite)); + doNothing().when(favoriteRepository).delete(any(Favorite.class)); + + // when + FavoriteResponseDto result = favoriteService.delete(1L, 1L); + + // then + assertNotNull(result); + assertThat(result).extracting("favoriteId", "memberId", "productId") + .containsExactly(1L, 1L, 1L); + + verify(memberService, times(1)).getMemberById(any(Long.TYPE)); + verify(productRepository, times(1)).findById(any(Long.TYPE)); + verify(favoriteRepository, times(1)) + .findByMemberAndProduct(any(Member.class), any(Product.class)); + verify(favoriteRepository, times(1)).delete(any(Favorite.class)); + } + } +} diff --git a/src/test/java/com/fc/shimpyo_be/domain/member/docs/AuthRestControllerDocsTest.java b/src/test/java/com/fc/shimpyo_be/domain/member/docs/AuthRestControllerDocsTest.java index b64be10f..e4350f54 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/member/docs/AuthRestControllerDocsTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/member/docs/AuthRestControllerDocsTest.java @@ -34,9 +34,9 @@ public class AuthRestControllerDocsTest extends RestDocsSupport { private final ConstraintDescriptions signUpDescriptions = new ConstraintDescriptions( SignUpRequestDto.class); private final ConstraintDescriptions signInDescriptions = new ConstraintDescriptions( - SignUpRequestDto.class); + SignInRequestDto.class); private final ConstraintDescriptions refreshDescriptions = new ConstraintDescriptions( - SignUpRequestDto.class); + RefreshRequestDto.class); @Test @DisplayName("signUp()은 회원 가입 할 수 있다.") @@ -57,7 +57,7 @@ void signUp() throws Exception { given(authService.signUp(any(SignUpRequestDto.class))).willReturn(memberResponseDto); - // when + // when then mockMvc.perform(post("/api/auth/signup") .content(objectMapper.writeValueAsString(signUpRequestDto)) .contentType(MediaType.APPLICATION_JSON)) @@ -118,7 +118,7 @@ void signIn() throws Exception { given(authService.signIn(any(SignInRequestDto.class))).willReturn(signInResponseDto); - // when + // when then mockMvc.perform(post("/api/auth/signin") .content(objectMapper.writeValueAsString(signInRequestDto)) .contentType(MediaType.APPLICATION_JSON)) @@ -187,7 +187,7 @@ void refresh() throws Exception { given(authService.refresh(any(RefreshRequestDto.class))).willReturn(signInResponseDto); - // when + // when then mockMvc.perform(post("/api/auth/refresh") .content(objectMapper.writeValueAsString(refreshRequestDto)) .contentType(MediaType.APPLICATION_JSON)) diff --git a/src/test/java/com/fc/shimpyo_be/domain/member/docs/MemberRestControllerDocsTest.java b/src/test/java/com/fc/shimpyo_be/domain/member/docs/MemberRestControllerDocsTest.java index 1b6f71ed..5b8b8da9 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/member/docs/MemberRestControllerDocsTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/member/docs/MemberRestControllerDocsTest.java @@ -1,5 +1,15 @@ package com.fc.shimpyo_be.domain.member.docs; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; + import com.fc.shimpyo_be.config.RestDocsSupport; import com.fc.shimpyo_be.domain.member.dto.request.CheckPasswordRequestDto; import com.fc.shimpyo_be.domain.member.dto.request.UpdateMemberRequestDto; @@ -15,14 +25,6 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doNothing; -import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; - public class MemberRestControllerDocsTest extends RestDocsSupport { @MockBean diff --git a/src/test/java/com/fc/shimpyo_be/domain/product/docs/ProductRestIntegrationDocsTest.java b/src/test/java/com/fc/shimpyo_be/domain/product/docs/ProductRestIntegrationDocsTest.java index b502a022..c033e407 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/product/docs/ProductRestIntegrationDocsTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/product/docs/ProductRestIntegrationDocsTest.java @@ -1,5 +1,7 @@ package com.fc.shimpyo_be.domain.product.docs; +import static org.junit.matchers.JUnitMatchers.everyItem; +import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; @@ -10,28 +12,34 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fc.shimpyo_be.config.RestDocsSupport; -import com.fc.shimpyo_be.domain.product.dto.response.ProductDetailsResponse; +import com.fc.shimpyo_be.domain.favorite.entity.Favorite; +import com.fc.shimpyo_be.domain.favorite.repository.FavoriteRepository; +import com.fc.shimpyo_be.domain.member.entity.Authority; +import com.fc.shimpyo_be.domain.member.entity.Member; +import com.fc.shimpyo_be.domain.member.repository.MemberRepository; import com.fc.shimpyo_be.domain.product.entity.Product; import com.fc.shimpyo_be.domain.product.entity.ProductImage; import com.fc.shimpyo_be.domain.product.factory.ProductFactory; import com.fc.shimpyo_be.domain.product.repository.ProductImageRepository; import com.fc.shimpyo_be.domain.product.repository.ProductRepository; -import com.fc.shimpyo_be.domain.product.util.ProductMapper; import com.fc.shimpyo_be.domain.room.entity.Room; import com.fc.shimpyo_be.domain.room.repository.RoomRepository; +import com.fc.shimpyo_be.global.util.SecurityUtil; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; @AutoConfigureMockMvc +@ActiveProfiles("test") class ProductRestIntegrationDocsTest extends RestDocsSupport { @@ -47,42 +55,28 @@ class ProductRestIntegrationDocsTest extends RestDocsSupport { @Autowired private RedisTemplate restTemplate; + @Autowired + private FavoriteRepository favoriteRepository; - private void assertProductDetailsResponse(Product expectedProduct, ResultActions resultActions) - throws Exception { - // then - ProductDetailsResponse expectedProductDetailsResponse = ProductMapper.toProductDetailsResponse( - expectedProduct); - - resultActions.andDo(MockMvcResultHandlers.print()).andExpect(status().is2xxSuccessful()) - .andExpect( - jsonPath("$.data.productId").value(expectedProductDetailsResponse.productId())) - .andExpect( - jsonPath("$.data.productName").value(expectedProductDetailsResponse.productName())) - .andExpect(jsonPath("$.data.address").value(expectedProductDetailsResponse.address())) - .andExpect(jsonPath("$.data.category").value(expectedProductDetailsResponse.category())) - .andExpect(jsonPath("$.data.starAvg").value(expectedProductDetailsResponse.starAvg())); - - - } + @Autowired + private MemberRepository memberRepository; + @MockBean + private SecurityUtil securityUtil; @DisplayName("숙소 저장 후, 검색 조회 및 페이징할 수 있다.") @Test - @WithMockUser void getProducts() throws Exception { // given - for (int i = 0; i < 20; i++) { + for (int i = 0; i < 5; i++) { Product product = productRepository.save(ProductFactory.createTestProduct()); - ProductImage productImage = productImageRepository.save( - ProductFactory.createTestProductImage(product)); - Room room = roomRepository.save(ProductFactory.createTestRoom(product)); + Room room = roomRepository.save(ProductFactory.createTestRoom(product, 0L)); product.getRooms().add(room); } // when ResultActions getProductAction = mockMvc.perform( - get("/api/products?page=0&size=20&address=서울시&category=호텔,모텔&productName=숙박")); + get("/api/products?page=0&size=20&capacity=5")); // then getProductAction.andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()).andDo( @@ -90,35 +84,43 @@ void getProducts() throws Exception { queryParameters(parameterWithName("page").optional().description("페이지 인덱스"), parameterWithName("size").optional().description("페이지 사이즈"), parameterWithName("sort").optional().description("정렬 할 컬럼 및 방향"), - parameterWithName("address").optional().description("상품 주소"), - parameterWithName("category").optional().description("상품 카테고리"), - parameterWithName("productName").optional().description("상품 이름")), + parameterWithName("address").optional().description("숙소 주소"), + parameterWithName("category").optional().description("숙소 카테고리"), + parameterWithName("productName").optional().description("숙소 이름"), + parameterWithName("capacity").optional().description("객실 최대 수용인원") + ), responseFields( fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답 코드"), fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), - fieldWithPath("data").type(JsonFieldType.ARRAY).description("응답 데이터"), - fieldWithPath("data[].productId").type(JsonFieldType.NUMBER) - .description("상품 아이디"), - fieldWithPath("data[].category").type(JsonFieldType.STRING) - .description("상품 카테고리(호텔, 모텔, 풀빌라, 펜션)"), - fieldWithPath("data[].address").type(JsonFieldType.STRING).description("상품 주소"), - fieldWithPath("data[].productName").type(JsonFieldType.STRING) - .description("상품 이름"), - fieldWithPath("data[].favorites").type(JsonFieldType.BOOLEAN) + fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), + fieldWithPath("data.productResponses").type(JsonFieldType.ARRAY) + .description("숙소 응답 데이터 배열"), + fieldWithPath("data.pageCount").type(JsonFieldType.NUMBER) + .description("총 페이지 개수"), + fieldWithPath("data.productResponses[].productId").type(JsonFieldType.NUMBER) + .description("숙소 아이디"), + fieldWithPath("data.productResponses[].category").type(JsonFieldType.STRING) + .description("숙소 카테고리(호텔, 모텔, 풀빌라, 펜션)"), + fieldWithPath("data.productResponses[].address").type(JsonFieldType.STRING) + .description("숙소 주소"), + fieldWithPath("data.productResponses[].productName").type(JsonFieldType.STRING) + .description("숙소 이름"), + fieldWithPath("data.productResponses[].favorites").type(JsonFieldType.BOOLEAN) .description("즐겨찾기"), - fieldWithPath("data[].starAvg").type(JsonFieldType.NUMBER).description("상품 평점"), - fieldWithPath("data[].image").type(JsonFieldType.STRING) - .description("상품 썸네일 이미지"), - fieldWithPath("data[].price").type(JsonFieldType.NUMBER) - .description("상품 내 방 최저 가격"), - fieldWithPath("data[].capacity").type(JsonFieldType.NUMBER) + fieldWithPath("data.productResponses[].starAvg").type(JsonFieldType.NUMBER) + .description("숙소 평점"), + fieldWithPath("data.productResponses[].image").type(JsonFieldType.STRING) + .description("숙소 썸네일 이미지"), + fieldWithPath("data.productResponses[].price").type(JsonFieldType.NUMBER) + .description("숙소 내 방 최저 가격"), + fieldWithPath("data.productResponses[].capacity").type(JsonFieldType.NUMBER) .description("최대 인원")))); } + @DisplayName("숙소 상세 검색을 할 수 있다.") @Test - @WithMockUser void getProductDetails() throws Exception { // given Product product = productRepository.save(ProductFactory.createTestProduct()); @@ -127,9 +129,14 @@ void getProductDetails() throws Exception { ProductImage productImage2 = productImageRepository.save( ProductFactory.createTestProductImage(product)); + for (int i = 0; i < 5; i++) { + Room room = roomRepository.save(ProductFactory.createTestRoom(product, 0L)); + product.getRooms().add(room); + + } for (int i = 0; i < 5; i++) { - Room room = roomRepository.save(ProductFactory.createTestRoom(product)); + Room room = roomRepository.save(ProductFactory.createTestRoom(product, 1L)); product.getRooms().add(room); } @@ -139,30 +146,128 @@ void getProductDetails() throws Exception { product.getId())); // then - assertProductDetailsResponse(product, getProductAction); - getProductAction.andDo( - restDoc.document(pathParameters(parameterWithName("productId").description("상품 아이디")), + getProductAction.andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()).andDo( + restDoc.document(pathParameters(parameterWithName("productId").description("숙소 아이디")), queryParameters(parameterWithName("startDate").description("상세 검색, 체크인 일"), parameterWithName("endDate").description("상세 검색, 체크아웃 일")), responseFields( fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답 코드"), fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), fieldWithPath("data.productId").type(JsonFieldType.NUMBER) - .description("상품 아이디"), + .description("숙소 식별자"), + fieldWithPath("data.productAmenityResponse").type(JsonFieldType.OBJECT) + .description("숙소 어메니티"), + fieldWithPath("data.address").type(JsonFieldType.OBJECT) + .description("숙소 주소"), + fieldWithPath("data.address.address").type(JsonFieldType.STRING) + .description("숙소 주소"), + fieldWithPath("data.address.detailAddress").type(JsonFieldType.STRING) + .description("숙소 상세 주소"), + fieldWithPath("data.address.mapX").type(JsonFieldType.NUMBER) + .description("숙소 x좌표"), + fieldWithPath("data.address.mapY").type(JsonFieldType.NUMBER) + .description("숙소 y좌표"), + fieldWithPath("data.productAmenityResponse.barbecue").type( + JsonFieldType.BOOLEAN) + .description("숙소 바베큐장 여부"), + fieldWithPath("data.productAmenityResponse.beauty").type(JsonFieldType.BOOLEAN) + .description("숙소 뷰티시설 여부"), + fieldWithPath("data.productAmenityResponse.beverage").type( + JsonFieldType.BOOLEAN) + .description("숙소 식음료장 여부"), + fieldWithPath("data.productAmenityResponse.bicycle").type(JsonFieldType.BOOLEAN) + .description("숙소 자전거 대여 여부"), + fieldWithPath("data.productAmenityResponse.campfire").type( + JsonFieldType.BOOLEAN) + .description("숙소 캠프파이어 여부"), + fieldWithPath("data.productAmenityResponse.fitness").type(JsonFieldType.BOOLEAN) + .description("숙소 휘트니스 센터 여부"), + fieldWithPath("data.productAmenityResponse.karaoke").type(JsonFieldType.BOOLEAN) + .description("숙소 노래방 여부"), + fieldWithPath("data.productAmenityResponse.publicBath").type( + JsonFieldType.BOOLEAN) + .description("숙소 공동 샤워실 여부"), + fieldWithPath("data.productAmenityResponse.publicPc").type( + JsonFieldType.BOOLEAN) + .description("숙소 공동 PC실 여부"), + fieldWithPath("data.productAmenityResponse.sauna").type(JsonFieldType.BOOLEAN) + .description("숙소 사우나 시설 여부"), + fieldWithPath("data.productAmenityResponse.sports").type(JsonFieldType.BOOLEAN) + .description("숙소 스포츠 시설 여부"), + fieldWithPath("data.productAmenityResponse.seminar").type(JsonFieldType.BOOLEAN) + .description("숙소 세미나실 여부"), + fieldWithPath("data.productOptionResponse").type(JsonFieldType.OBJECT) + .description("숙소 옵션"), + fieldWithPath("data.productOptionResponse.cooking").type(JsonFieldType.BOOLEAN) + .description("객실 내 취사 여부"), + fieldWithPath("data.productOptionResponse.parking").type(JsonFieldType.BOOLEAN) + .description("숙소 주차 시설 여부"), + fieldWithPath("data.productOptionResponse.pickup").type(JsonFieldType.BOOLEAN) + .description("숙소 픽업 서비스 여부"), + fieldWithPath("data.productOptionResponse.foodPlace").type(JsonFieldType.STRING) + .description("숙소 식음료장"), + fieldWithPath("data.productOptionResponse.infoCenter").type( + JsonFieldType.STRING) + .description("숙소 문의 및 안내 번호"), fieldWithPath("data.category").type(JsonFieldType.STRING) - .description("상품 카테고리(호텔, 모텔, 풀빌라, 펜션)"), - fieldWithPath("data.address").type(JsonFieldType.STRING).description("상품 주소"), + .description("숙소 카테고리(호텔, 모텔, 풀빌라, 펜션)"), fieldWithPath("data.productName").type(JsonFieldType.STRING) - .description("상품 이름"), + .description("숙소 이름"), fieldWithPath("data.description").type(JsonFieldType.STRING) - .description("상품 설명"), + .description("숙소 설명"), fieldWithPath("data.favorites").type(JsonFieldType.BOOLEAN).description("즐겨찾기"), - fieldWithPath("data.starAvg").type(JsonFieldType.NUMBER).description("상품 평점"), - fieldWithPath("data.images").type(JsonFieldType.ARRAY).description("상품 관련 이미지"), + fieldWithPath("data.starAvg").type(JsonFieldType.NUMBER).description("숙소 평점"), + fieldWithPath("data.images").type(JsonFieldType.ARRAY).description("숙소 관련 이미지"), fieldWithPath("data.rooms").type(JsonFieldType.ARRAY) - .description("상품 하위 방 데이터"), - fieldWithPath("data.rooms[].roomId").type(JsonFieldType.NUMBER) - .description("방 아이디"), + .description("숙소 하위 방 데이터"), + fieldWithPath("data.rooms[].roomCode").type(JsonFieldType.NUMBER) + .description("객실 코드"), + fieldWithPath("data.rooms[].remaining").type(JsonFieldType.NUMBER) + .description("객실 남은수량"), + fieldWithPath("data.rooms[].roomImages[]").type(JsonFieldType.ARRAY) + .description("객실 이미지"), + fieldWithPath("data.rooms[].roomOptionResponse").type(JsonFieldType.OBJECT) + .description("방 옵션"), + fieldWithPath("data.rooms[].roomOptionResponse.bathFacility").type( + JsonFieldType.BOOLEAN) + .description("객실 내 목욕 시설 여부"), + fieldWithPath("data.rooms[].roomOptionResponse.bath").type( + JsonFieldType.BOOLEAN) + .description("객실 내 욕조 여부"), + fieldWithPath("data.rooms[].roomOptionResponse.homeTheater").type( + JsonFieldType.BOOLEAN) + .description("객실 내 홈시어터 여부"), + fieldWithPath("data.rooms[].roomOptionResponse.airCondition").type( + JsonFieldType.BOOLEAN) + .description("객실 내 에어컨 여부"), + fieldWithPath("data.rooms[].roomOptionResponse.tv").type(JsonFieldType.BOOLEAN) + .description("객실 내 TV 여부"), + fieldWithPath("data.rooms[].roomOptionResponse.pc").type(JsonFieldType.BOOLEAN) + .description("객실 내 PC 여부"), + fieldWithPath("data.rooms[].roomOptionResponse.cable").type( + JsonFieldType.BOOLEAN) + .description("객실 내 케이블 설치 여부"), + fieldWithPath("data.rooms[].roomOptionResponse.internet").type( + JsonFieldType.BOOLEAN) + .description("객실 내 인터넷 여부"), + fieldWithPath("data.rooms[].roomOptionResponse.refrigerator").type( + JsonFieldType.BOOLEAN) + .description("객실 내 냉장고 여부"), + fieldWithPath("data.rooms[].roomOptionResponse.toiletries").type( + JsonFieldType.BOOLEAN) + .description("객실 내 세면도구 여부"), + fieldWithPath("data.rooms[].roomOptionResponse.sofa").type( + JsonFieldType.BOOLEAN) + .description("객실 내 소파 여부"), + fieldWithPath("data.rooms[].roomOptionResponse.cooking").type( + JsonFieldType.BOOLEAN) + .description("객실 내 취사용품 여부"), + fieldWithPath("data.rooms[].roomOptionResponse.table").type( + JsonFieldType.BOOLEAN) + .description("객실 내 테이블 여부"), + fieldWithPath("data.rooms[].roomOptionResponse.hairDryer").type( + JsonFieldType.BOOLEAN) + .description("객실 내 드라이기 여부"), fieldWithPath("data.rooms[].roomName").type(JsonFieldType.STRING) .description("방 이름"), fieldWithPath("data.rooms[].price").type(JsonFieldType.NUMBER) @@ -176,26 +281,24 @@ void getProductDetails() throws Exception { fieldWithPath("data.rooms[].checkIn").type(JsonFieldType.STRING) .description("체크인 시간"), fieldWithPath("data.rooms[].checkOut").type(JsonFieldType.STRING) - .description("체크아웃 시간"), - fieldWithPath("data.rooms[].reserved").type(JsonFieldType.BOOLEAN) - .description("예약 여부")))); + .description("체크아웃 시간")))); } @Test @DisplayName("예약 가능 여부를 확인할 수 있다.") - @WithMockUser void isAvailableForReservation() throws Exception { // given Product product = productRepository.save(ProductFactory.createTestProduct()); - Room room = roomRepository.save(ProductFactory.createTestRoom(product)); + Room room = roomRepository.save(ProductFactory.createTestRoom(product, 0L)); product.getRooms().add(room); ValueOperations values = restTemplate.opsForValue(); - values.set("roomId:" + String.valueOf(room.getId()) + ":" + "2023-12-22", "OK"); + values.set("roomId:" + room.getId() + ":" + "2023-12-22", "OK"); // when ResultActions getProductAction = mockMvc.perform( - get("/api/products/amounts/{roomId}?startDate=2023-12-22&endDate=2023-12-23",room.getId())); + get("/api/products/amounts/{roomId}?startDate=2023-12-22&endDate=2023-12-23", + room.getId())); // then getProductAction @@ -217,5 +320,34 @@ void isAvailableForReservation() throws Exception { )); } + @Test + @DisplayName("전체 조회에서 즐겨찾기 여부를 볼 수 있다.") + void isAvailableGetFavoriteInGetProducts() throws Exception { + //given + given(securityUtil.getNullableCurrentMemberId()).willReturn(1L); + Product product = productRepository.save(ProductFactory.createTestProduct()); + Room room = roomRepository.save(ProductFactory.createTestRoom(product, 0L)); + Member member = Member.builder() + .email("test@mail.com") + .name("test") + .password("$10$ygrAExVYmFTkZn2d0.Pk3Ot5CNZwIBjZH5f.WW0AnUq4w4PtBi9Nm") + .photoUrl( + "https://fastly.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI") + .authority(Authority.ROLE_USER) + .build(); + memberRepository.save(member); + Favorite favorite = Favorite.builder().product(product).member(member).build(); + favoriteRepository.save(favorite); + + // when + ResultActions getProductAction = mockMvc.perform( + get("/api/products")); + + //then + getProductAction + .andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()).andExpect(jsonPath("$.data.productResponses[0].favorites").value(true)); + + } + } \ No newline at end of file diff --git a/src/test/java/com/fc/shimpyo_be/domain/product/factory/ProductFactory.java b/src/test/java/com/fc/shimpyo_be/domain/product/factory/ProductFactory.java index 4c67937a..226e5d40 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/product/factory/ProductFactory.java +++ b/src/test/java/com/fc/shimpyo_be/domain/product/factory/ProductFactory.java @@ -1,14 +1,19 @@ package com.fc.shimpyo_be.domain.product.factory; -import java.util.*; +import com.fc.shimpyo_be.domain.product.entity.Address; +import com.fc.shimpyo_be.domain.product.entity.Amenity; import com.fc.shimpyo_be.domain.product.entity.Category; import com.fc.shimpyo_be.domain.product.entity.Product; import com.fc.shimpyo_be.domain.product.entity.ProductImage; +import com.fc.shimpyo_be.domain.product.entity.ProductOption; import com.fc.shimpyo_be.domain.product.model.Area; import com.fc.shimpyo_be.domain.product.model.RandomProductInfo; import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.domain.room.entity.RoomOption; +import com.fc.shimpyo_be.domain.room.entity.RoomPrice; import java.time.LocalTime; import java.util.ArrayList; +import java.util.List; import java.util.concurrent.ThreadLocalRandom; public class ProductFactory { @@ -17,11 +22,39 @@ public class ProductFactory { public static Product createTestProduct() { String area = Area.values()[ThreadLocalRandom.current() .nextInt(Area.values().length)].toString(); - return Product.builder().name(area + " 숙박").address("서울시" + area) + return Product.builder().name(area + " 숙박").address(Address.builder() + .address("서울시" + area) + .detailAddress("상세주소") + .mapX(1.0) + .mapY(1.0) + .build()) .thumbnail(RandomProductInfo.genRandomImage()).category( Category.values()[ThreadLocalRandom.current().nextInt(Category.values().length)]) .starAvg(ThreadLocalRandom.current().nextFloat(5)) - .description(RandomProductInfo.genRandomDescription()).build(); + .description(RandomProductInfo.genRandomDescription()) + .rooms(new ArrayList<>()) + .productOption(ProductOption.builder() + .cooking(false) + .parking(false) + .pickup(false) + .foodPlace("") + .infoCenter("000-0000-0000") + .build()) + .amenity(Amenity.builder() + .barbecue(false) + .beauty(false) + .beverage(false) + .bicycle(false) + .campfire(false) + .karaoke(false) + .publicBath(false) + .publicPc(false) + .sauna(false) + .seminar(false) + .sports(false) + .fitness(false) + .build()) + .build(); } public static ProductImage createTestProductImage(Product product) { @@ -29,15 +62,40 @@ public static ProductImage createTestProductImage(Product product) { .build(); } - public static Room createTestRoom(Product product) { + public static Room createTestRoom(Product product, Long code) { int stadard = ThreadLocalRandom.current().nextInt(10); + int fee = ThreadLocalRandom.current().nextInt(100000); - return Room.builder().price(ThreadLocalRandom.current().nextInt(100000)) + return Room.builder().price(RoomPrice.builder() + .offWeekDaysMinFee(fee) + .offWeekendMinFee(fee + 10000) + .peakWeekDaysMinFee(fee + 50000) + .peakWeekendMinFee(fee + 60000) + .build()) .description(RandomProductInfo.genRandomDescription()).product(product) .checkIn(LocalTime.of(11, 0, 0)).checkOut(LocalTime.of(15, 0, 0)) .name(product.getCategory().getName() + " 방").standard(stadard) - .capacity(stadard + ThreadLocalRandom.current().nextInt(10)).build(); + .capacity(stadard + ThreadLocalRandom.current().nextInt(10)) + .roomImages(new ArrayList<>()) + .code(code) + .roomOption(RoomOption.builder() + .bathFacility(false) + .bath(false) + .homeTheater(false) + .airCondition(false) + .tv(false) + .pc(false) + .cable(false) + .internet(false) + .refrigerator(false) + .toiletries(false) + .sofa(false) + .cooking(false) + .diningTable(false) + .hairDryer(false) + .build()) + .build(); } diff --git a/src/test/java/com/fc/shimpyo_be/domain/product/unit/controller/ProductRestControllerTest.java b/src/test/java/com/fc/shimpyo_be/domain/product/unit/controller/ProductRestControllerTest.java index 4893d6e6..b279d066 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/product/unit/controller/ProductRestControllerTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/product/unit/controller/ProductRestControllerTest.java @@ -7,6 +7,7 @@ import com.fc.shimpyo_be.domain.product.controller.ProductRestController; import com.fc.shimpyo_be.domain.product.dto.request.SearchKeywordRequest; +import com.fc.shimpyo_be.domain.product.dto.response.PaginatedProductResponse; import com.fc.shimpyo_be.domain.product.dto.response.ProductDetailsResponse; import com.fc.shimpyo_be.domain.product.dto.response.ProductResponse; import com.fc.shimpyo_be.domain.product.entity.Category; @@ -40,17 +41,25 @@ class ProductRestControllerTest { void getAllProducts() { //given List productResponses = new ArrayList<>(); - productResponses.add(ProductMapper.toProductResponse(ProductFactory.createTestProduct())); + productResponses.add(ProductMapper.toProductResponse(ProductFactory.createTestProduct(),false)); + PaginatedProductResponse paginatedProductResponse = PaginatedProductResponse.builder() + .productResponses(productResponses) + .pageCount(1) + .build(); + SearchKeywordRequest searchKeywordRequest = SearchKeywordRequest.builder() + .address("") + .productName("") + .capacity(0L) .category(Category.MOTEL.getName()).build(); Pageable pageable = Pageable.ofSize(10); - doReturn(productResponses).when(productService).getProducts(searchKeywordRequest, pageable); - ResponseEntity>> result = productRestController.getProducts( + doReturn(paginatedProductResponse).when(productService).getProducts(searchKeywordRequest, pageable); + ResponseEntity> result = productRestController.getProducts( searchKeywordRequest.productName(), searchKeywordRequest.address(), - searchKeywordRequest.category(), pageable); + searchKeywordRequest.category().get(0).getName(),searchKeywordRequest.capacity(), pageable); //then assertEquals(result.getStatusCode(), HttpStatus.OK); - assertThat(result.getBody().getData()).usingRecursiveComparison() + assertThat(result.getBody().getData().productResponses()).usingRecursiveComparison() .isEqualTo(productResponses); } @@ -58,7 +67,7 @@ void getAllProducts() { void getProductDetails() { //given Product product = ProductFactory.createTestProduct(); - ProductDetailsResponse expectedResult = ProductMapper.toProductDetailsResponse(product); + ProductDetailsResponse expectedResult = ProductMapper.toProductDetailsResponse(product,false); doReturn(expectedResult).when(productService) .getProductDetails(1L, "2024-12-27", "2024-12-28"); //when diff --git a/src/test/java/com/fc/shimpyo_be/domain/product/unit/repository/ProductRepositoryTest.java b/src/test/java/com/fc/shimpyo_be/domain/product/unit/repository/ProductRepositoryTest.java new file mode 100644 index 00000000..79a750a1 --- /dev/null +++ b/src/test/java/com/fc/shimpyo_be/domain/product/unit/repository/ProductRepositoryTest.java @@ -0,0 +1,65 @@ +package com.fc.shimpyo_be.domain.product.unit.repository; + + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.fc.shimpyo_be.config.TestJpaConfig; +import com.fc.shimpyo_be.config.TestQuerydslConfig; +import com.fc.shimpyo_be.domain.product.dto.request.SearchKeywordRequest; +import com.fc.shimpyo_be.domain.product.entity.Product; +import com.fc.shimpyo_be.domain.product.factory.ProductFactory; +import com.fc.shimpyo_be.domain.product.repository.ProductCustomRepository; +import com.fc.shimpyo_be.domain.product.repository.ProductCustomRepositoryImpl; +import com.fc.shimpyo_be.domain.product.repository.ProductRepository; +import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.domain.room.repository.RoomRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + + +@DataJpaTest +@ComponentScan(basePackages = {"com.fc.shimpyo_be.domain.product.repository"}) +@Import({TestJpaConfig.class, TestQuerydslConfig.class}) +public class ProductRepositoryTest { + + @Autowired + ProductCustomRepositoryImpl productCustomRepository; + + @Autowired + ProductRepository productRepository; + + @Autowired + RoomRepository roomRepository; + + + @Test + public void getProducts() { + //given + Product product = ProductFactory.createTestProduct(); + Room room = ProductFactory.createTestRoom(product,0l); + + productRepository.save(product); + roomRepository.save(room); + + SearchKeywordRequest searchKeywordRequest = SearchKeywordRequest.builder() + .productName("") + .address("") + .category("") + .capacity(0L) + .build(); + + //when + Page products = productCustomRepository.findAllBySearchKeywordRequest( + searchKeywordRequest, + Pageable.ofSize(10)); + + //the + assertEquals(1, products.getContent().size()); + } + +} \ No newline at end of file diff --git a/src/test/java/com/fc/shimpyo_be/domain/product/unit/service/ProductServiceTest.java b/src/test/java/com/fc/shimpyo_be/domain/product/unit/service/ProductServiceTest.java index 187bcecf..ce5fd2fa 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/product/unit/service/ProductServiceTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/product/unit/service/ProductServiceTest.java @@ -2,21 +2,27 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doReturn; import com.fc.shimpyo_be.domain.product.dto.request.SearchKeywordRequest; +import com.fc.shimpyo_be.domain.product.dto.response.PaginatedProductResponse; import com.fc.shimpyo_be.domain.product.dto.response.ProductDetailsResponse; -import com.fc.shimpyo_be.domain.product.dto.response.ProductResponse; import com.fc.shimpyo_be.domain.product.entity.Category; import com.fc.shimpyo_be.domain.product.entity.Product; import com.fc.shimpyo_be.domain.product.factory.ProductFactory; +import com.fc.shimpyo_be.domain.product.repository.ProductCustomRepositoryImpl; import com.fc.shimpyo_be.domain.product.repository.ProductRepository; import com.fc.shimpyo_be.domain.product.service.ProductService; import com.fc.shimpyo_be.domain.product.util.ProductMapper; +import com.fc.shimpyo_be.domain.room.dto.response.RoomResponse; import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.global.util.SecurityUtil; 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; @@ -26,18 +32,27 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.domain.Specification; @ExtendWith(MockitoExtension.class) class ProductServiceTest { + @Mock + private SecurityUtil securityUtil; + @Mock private ProductRepository productRepository; + @Mock + private ProductCustomRepositoryImpl productCustomRepository; + @Spy @InjectMocks private ProductService productService; + @BeforeEach + void init() { + given(securityUtil.getNullableCurrentMemberId()).willReturn(null); + } @Test void getProducts() { @@ -46,39 +61,44 @@ void getProducts() { .productName("강릉 세인트 호텔").category(Category.MOTEL.getName()).address("강원도 강릉시 창해로 307") .build(); - Specification spec = (root, query, criteriaBuilder) -> null; - List expectedProducts = ProductFactory.createTestProducts(); Pageable pageable = Pageable.ofSize(10); Page productPage = new PageImpl<>(expectedProducts); - given(productRepository.findAll(any(Specification.class), any(Pageable.class))).willReturn( + given(productCustomRepository.findAllBySearchKeywordRequest(any(SearchKeywordRequest.class), + any(Pageable.class))).willReturn( productPage); - //when - List result = productService.getProducts(searchKeywordRequest, pageable); + PaginatedProductResponse result = productService.getProducts(searchKeywordRequest, + pageable); //then - assertThat(result).usingRecursiveAssertion().isEqualTo( - productPage.getContent().stream().map(ProductMapper::toProductResponse).toList()); - + assertThat(result.productResponses()).usingRecursiveAssertion().isEqualTo( + productPage.getContent().stream() + .map(product -> ProductMapper.toProductResponse(product, false)).toList()); } @Test void getProductDetails() { //given Product product = ProductFactory.createTestProduct(); - Room room = ProductFactory.createTestRoom(product); + Room room = ProductFactory.createTestRoom(product, 0L); product.getRooms().add(room); given(productRepository.findById(product.getId())).willReturn(Optional.ofNullable(product)); - doReturn(true).when( - productService).isAvailableForReservation(product.getId(), "2023-11-27", "2023-11-28"); + doReturn(1L).when( + productService) + .countAvailableForReservationUsingRoomCode(anyLong(), anyString(), anyString()); + //when ProductDetailsResponse result = productService.getProductDetails(product.getId(), "2023-11-27", "2023-11-28"); //then for (int i = 0; i < result.rooms().size(); i++) { + RoomResponse roomResponse = ProductMapper.toProductDetailsResponse(product, false) + .rooms() + .get(i); + roomResponse.setRemaining(1L); assertThat(result.rooms().get(i)).usingRecursiveComparison() - .isEqualTo(ProductMapper.toProductDetailsResponse(product).rooms().get(i)); + .isEqualTo(roomResponse); } } diff --git a/src/test/java/com/fc/shimpyo_be/domain/reservation/docs/ReservationRestControllerDocsTest.java b/src/test/java/com/fc/shimpyo_be/domain/reservation/docs/ReservationRestControllerDocsTest.java index 35cdd980..1dee7527 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/reservation/docs/ReservationRestControllerDocsTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/reservation/docs/ReservationRestControllerDocsTest.java @@ -4,12 +4,14 @@ import com.fc.shimpyo_be.domain.reservation.dto.request.*; import com.fc.shimpyo_be.domain.reservation.dto.response.ReservationInfoResponseDto; import com.fc.shimpyo_be.domain.reservation.dto.response.SaveReservationResponseDto; -import com.fc.shimpyo_be.domain.reservation.dto.response.ValidationResultResponseDto; +import com.fc.shimpyo_be.domain.reservation.dto.response.ValidatePreoccupyResultResponseDto; +import com.fc.shimpyo_be.domain.reservation.dto.response.ValidatePreoccupyRoomResponseDto; import com.fc.shimpyo_be.domain.reservation.entity.PayMethod; import com.fc.shimpyo_be.domain.reservation.facade.PreoccupyRoomsLockFacade; import com.fc.shimpyo_be.domain.reservation.facade.ReservationLockFacade; import com.fc.shimpyo_be.domain.reservation.service.ReservationService; import com.fc.shimpyo_be.domain.reservationproduct.dto.request.ReservationProductRequestDto; +import com.fc.shimpyo_be.domain.reservationproduct.dto.response.ReservationProductResponseDto; import com.fc.shimpyo_be.global.util.SecurityUtil; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -22,7 +24,6 @@ import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.security.test.context.support.WithMockUser; -import java.util.ArrayList; import java.util.List; import static org.mockito.ArgumentMatchers.any; @@ -79,24 +80,72 @@ void saveReservation() throws Exception { String requestUrl = "/api/reservations"; SaveReservationRequestDto requestDto - = new SaveReservationRequestDto( - List.of( - new ReservationProductRequestDto( - 1L, "신라호텔", "디럭스 더블1", - 2, 4, "2023-11-20", "2023-11-23", - "13:00", "12:00", - "홍길동", "010-1111-1111", 300000 - ), - new ReservationProductRequestDto( - 3L, "강릉 고즈넉한 펜션", "숲의 방", - 6, 9, "2023-12-10", "2023-12-12", - "13:00", "12:00", - "김갑돌", "010-2222-2222", 150000 + = SaveReservationRequestDto.builder() + .reservationProducts( + List.of( + ReservationProductRequestDto.builder() + .cartId(2L) + .roomId(1L) + .startDate("2023-11-20") + .endDate("2023-11-23") + .visitorName("visitor1") + .visitorPhone("010-1111-1111") + .price(300000) + .build(), + ReservationProductRequestDto.builder() + .cartId(7L) + .roomId(2L) + .startDate("2023-12-10") + .endDate("2023-12-12") + .visitorName("visitor2") + .visitorPhone("010-2222-2222") + .price(150000) + .build() ) - ), PayMethod.CREDIT_CARD, 450000 - ); - - SaveReservationResponseDto responseDto = new SaveReservationResponseDto(1L, requestDto); + ) + .payMethod(PayMethod.CREDIT_CARD) + .totalPrice(450000) + .build(); + + SaveReservationResponseDto responseDto + = SaveReservationResponseDto.builder() + .reservationId(1L) + .reservationProducts( + List.of( + ReservationProductResponseDto.builder() + .productName("숙소1") + .roomId(1L) + .roomName("객실1") + .standard(2) + .capacity(3) + .startDate("2023-11-20") + .endDate("2023-11-23") + .checkIn("13:00") + .checkOut("12:00") + .visitorName("visitor1") + .visitorPhone("010-1111-1111") + .price(300000) + .build(), + ReservationProductResponseDto.builder() + .productName("숙소2") + .roomId(2L) + .roomName("객실2") + .standard(2) + .capacity(3) + .startDate("2023-12-10") + .endDate("2023-12-1") + .checkIn("13:00") + .checkOut("12:00") + .visitorName("visitor2") + .visitorPhone("010-2222-2222") + .price(150000) + .build() + ) + ) + .payMethod(requestDto.payMethod()) + .totalPrice(requestDto.totalPrice()) + .createdAt("2023-12-06 10:30:35") + .build(); given(securityUtil.getCurrentMemberId()).willReturn(1L); given(reservationLockFacade.saveReservation(anyLong(), any(SaveReservationRequestDto.class))) @@ -109,36 +158,21 @@ void saveReservation() throws Exception { .andExpect(status().isCreated()) .andDo(restDoc.document( requestFields( - fieldWithPath("reservationProducts").type(JsonFieldType.ARRAY).description("예약할 객실 상품 리스트") + fieldWithPath("reservationProducts").type(JsonFieldType.ARRAY).description("예약할 객실 숙소 리스트") .attributes(key("constraints").value( saveReservationDescriptions.descriptionsForProperty("reservationProducts"))), + fieldWithPath("reservationProducts[].cartId").type(JsonFieldType.NUMBER).description("장바구니 식별자") + .attributes(key("constraints").value( + reservationProductDescriptions.descriptionsForProperty("cartId"))), fieldWithPath("reservationProducts[].roomId").type(JsonFieldType.NUMBER).description("예약할 객실 식별자") .attributes(key("constraints").value( reservationProductDescriptions.descriptionsForProperty("roomId"))), - fieldWithPath("reservationProducts[].productName").type(JsonFieldType.STRING).description("예약할 숙소명") - .attributes(key("constraints").value( - reservationProductDescriptions.descriptionsForProperty("productName"))), - fieldWithPath("reservationProducts[].roomName").type(JsonFieldType.STRING).description("예약할 객실명") - .attributes(key("constraints").value( - reservationProductDescriptions.descriptionsForProperty("roomName"))), - fieldWithPath("reservationProducts[].standard").type(JsonFieldType.NUMBER).description("객실 기준 인원") - .attributes(key("constraints").value( - reservationProductDescriptions.descriptionsForProperty("standard"))), - fieldWithPath("reservationProducts[].max").type(JsonFieldType.NUMBER).description("객실 최대 인원") - .attributes(key("constraints").value( - reservationProductDescriptions.descriptionsForProperty("max"))), fieldWithPath("reservationProducts[].startDate").type(JsonFieldType.STRING).description("숙박 시작일") .attributes(key("constraints").value( reservationProductDescriptions.descriptionsForProperty("startDate"))), fieldWithPath("reservationProducts[].endDate").type(JsonFieldType.STRING).description("숙박 마지막일") .attributes(key("constraints").value( reservationProductDescriptions.descriptionsForProperty("endDate"))), - fieldWithPath("reservationProducts[].checkIn").type(JsonFieldType.STRING).description("체크인 시간") - .attributes(key("constraints").value( - reservationProductDescriptions.descriptionsForProperty("checkIn"))), - fieldWithPath("reservationProducts[].checkOut").type(JsonFieldType.STRING).description("체크아웃 시간") - .attributes(key("constraints").value( - reservationProductDescriptions.descriptionsForProperty("checkOut"))), fieldWithPath("reservationProducts[].visitorName").type(JsonFieldType.STRING).description("방문자명") .attributes(key("constraints").value( reservationProductDescriptions.descriptionsForProperty("visitorName"))), @@ -159,12 +193,12 @@ void saveReservation() throws Exception { responseFields(responseCommon()).and( fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), fieldWithPath("data.reservationId").type(JsonFieldType.NUMBER).description("예약 식별자"), - fieldWithPath("data.reservationProducts").type(JsonFieldType.ARRAY).description("예약 상품 리스트"), - fieldWithPath("data.reservationProducts[].roomId").type(JsonFieldType.NUMBER).description("객실 식별자"), + fieldWithPath("data.reservationProducts").type(JsonFieldType.ARRAY).description("예약 숙소 리스트"), fieldWithPath("data.reservationProducts[].productName").type(JsonFieldType.STRING).description("숙소명"), + fieldWithPath("data.reservationProducts[].roomId").type(JsonFieldType.NUMBER).description("객실 식별자"), fieldWithPath("data.reservationProducts[].roomName").type(JsonFieldType.STRING).description("객실명"), fieldWithPath("data.reservationProducts[].standard").type(JsonFieldType.NUMBER).description("기준 인원"), - fieldWithPath("data.reservationProducts[].max").type(JsonFieldType.NUMBER).description("최대 인원"), + fieldWithPath("data.reservationProducts[].capacity").type(JsonFieldType.NUMBER).description("최대 인원"), fieldWithPath("data.reservationProducts[].startDate").type(JsonFieldType.STRING).description("숙박 시작일"), fieldWithPath("data.reservationProducts[].endDate").type(JsonFieldType.STRING).description("숙박 마지막일"), fieldWithPath("data.reservationProducts[].checkIn").type(JsonFieldType.STRING).description("체크인 시간"), @@ -173,8 +207,9 @@ void saveReservation() throws Exception { fieldWithPath("data.reservationProducts[].visitorPhone").type(JsonFieldType.STRING).description("방문자 전화번호"), fieldWithPath("data.reservationProducts[].price").type(JsonFieldType.NUMBER).description("객실 이용 가격"), fieldWithPath("data.payMethod").type(JsonFieldType.STRING).description("결제 수단"), - fieldWithPath("data.totalPrice").type(JsonFieldType.NUMBER).description("총 결제 금액") - ) + fieldWithPath("data.totalPrice").type(JsonFieldType.NUMBER).description("총 결제 금액"), + fieldWithPath("data.createdAt").type(JsonFieldType.STRING).description("예약 주문 시간") + ) ) ); @@ -191,25 +226,27 @@ void getReservationInfoList() throws Exception { int page = 0; PageRequest pageRequest = PageRequest.of(page, size); - List content - = List.of( - new ReservationInfoResponseDto( - 2L, - 3L, - 5L, - "호텔1", - "호텔1 photoUrl", - "호텔1 주소 url", - 1L, - "객실1", - "2023-11-23", - "2023-11-26", - "14:00", - "12:00", - 220000, - "CREDIT_CARD" - ) - ); + List content = + List.of( + ReservationInfoResponseDto.builder() + .reservationId(2L) + .reservationProductId(3L) + .productId(5L) + .productName("호텔1") + .productImageUrl("호텔1 photo URL") + .productAddress("호텔1 주소") + .productDetailAddress("호텔 상세 주소") + .roomId(1L) + .roomName("객실1") + .startDate("2023-11-23") + .endDate("2023-11-26") + .checkIn("14:00") + .checkOut("12:00") + .price(220000) + .payMethod("CREDIT_CARD") + .createdAt("2023-11-20 10:00:00") + .build() + ); given(securityUtil.getCurrentMemberId()).willReturn(1L); given(reservationService.getReservationInfoList(anyLong(), any(Pageable.class))) @@ -231,20 +268,22 @@ void getReservationInfoList() throws Exception { responseFields(responseCommon()).and( fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), fieldWithPath("data.content").type(JsonFieldType.ARRAY).description("조회 데이터 리스트"), - fieldWithPath("data.content.[].reservationId").type(JsonFieldType.NUMBER).description("예약 식별자"), - fieldWithPath("data.content.[].reservationProductId").type(JsonFieldType.NUMBER).description("예약 상품 식별자"), - fieldWithPath("data.content.[].productId").type(JsonFieldType.NUMBER).description("상품 식별자"), - fieldWithPath("data.content.[].productName").type(JsonFieldType.STRING).description("숙소명"), - fieldWithPath("data.content.[].productImageUrl").type(JsonFieldType.STRING).description("숙소 대표 이미지 URL"), - fieldWithPath("data.content.[].productAddress").type(JsonFieldType.STRING).description("숙소 주소"), - fieldWithPath("data.content.[].roomId").type(JsonFieldType.NUMBER).description("예약한 객실 식별자"), - fieldWithPath("data.content.[].roomName").type(JsonFieldType.STRING).description("객실명"), - fieldWithPath("data.content.[].startDate").type(JsonFieldType.STRING).description("숙박 시작일"), - fieldWithPath("data.content.[].endDate").type(JsonFieldType.STRING).description("숙박 마지막일"), - fieldWithPath("data.content.[].checkIn").type(JsonFieldType.STRING).description("체크인 시간"), - fieldWithPath("data.content.[].checkOut").type(JsonFieldType.STRING).description("체크아웃 시간"), - fieldWithPath("data.content.[].price").type(JsonFieldType.NUMBER).description("결제 금액"), - fieldWithPath("data.content.[].payMethod").type(JsonFieldType.STRING).description("결제 수단"), + fieldWithPath("data.content[].reservationId").type(JsonFieldType.NUMBER).description("예약 식별자"), + fieldWithPath("data.content[].reservationProductId").type(JsonFieldType.NUMBER).description("예약 숙소 식별자"), + fieldWithPath("data.content[].productId").type(JsonFieldType.NUMBER).description("숙소 식별자"), + fieldWithPath("data.content[].productName").type(JsonFieldType.STRING).description("숙소명"), + fieldWithPath("data.content[].productImageUrl").type(JsonFieldType.STRING).description("숙소 대표 이미지 URL"), + fieldWithPath("data.content[].productAddress").type(JsonFieldType.STRING).description("숙소 주소"), + fieldWithPath("data.content[].productDetailAddress").type(JsonFieldType.STRING).description("숙소 상세 주소"), + fieldWithPath("data.content[].roomId").type(JsonFieldType.NUMBER).description("예약한 객실 식별자"), + fieldWithPath("data.content[].roomName").type(JsonFieldType.STRING).description("객실명"), + fieldWithPath("data.content[].startDate").type(JsonFieldType.STRING).description("숙박 시작일"), + fieldWithPath("data.content[].endDate").type(JsonFieldType.STRING).description("숙박 마지막일"), + fieldWithPath("data.content[].checkIn").type(JsonFieldType.STRING).description("체크인 시간"), + fieldWithPath("data.content[].checkOut").type(JsonFieldType.STRING).description("체크아웃 시간"), + fieldWithPath("data.content[].price").type(JsonFieldType.NUMBER).description("결제 금액"), + fieldWithPath("data.content[].payMethod").type(JsonFieldType.STRING).description("결제 수단"), + fieldWithPath("data.content[].createdAt").type(JsonFieldType.STRING).description("예약 결제 완료 일시"), fieldWithPath("data.pageable.sort.sorted").type(JsonFieldType.BOOLEAN).description("정렬 여부"), fieldWithPath("data.pageable.sort.empty").type(JsonFieldType.BOOLEAN).description("데이터가 비었는지 여부"), @@ -280,22 +319,47 @@ void checkAvailableAndPreoccupy() throws Exception { // given String requestUrl = "/api/reservations/preoccupy"; - PreoccupyRoomsRequestDto requestDto - = new PreoccupyRoomsRequestDto( - List.of( - new PreoccupyRoomItemRequestDto(1L, "2023-12-23", "2023-12-25"), - new PreoccupyRoomItemRequestDto(2L, "2023-11-11", "2023-11-14") - ) - ); - - ValidationResultResponseDto responseDto - = new ValidationResultResponseDto(true, new ArrayList<>()); + PreoccupyRoomsRequestDto requestDto = + PreoccupyRoomsRequestDto.builder() + .rooms( + List.of( + PreoccupyRoomItemRequestDto.builder() + .cartId(1L).roomCode(1001L).startDate("2023-12-23").endDate("2023-12-25") + .build(), + PreoccupyRoomItemRequestDto.builder() + .cartId(2L).roomCode(1002L).startDate("2023-11-11").endDate("2023-11-14") + .build() + ) + ) + .build(); + + ValidatePreoccupyResultResponseDto responseDto = + ValidatePreoccupyResultResponseDto.builder() + .isAvailable(true) + .roomResults( + List.of( + ValidatePreoccupyRoomResponseDto.builder() + .cartId(1L) + .roomCode(1001L) + .startDate("2023-12-23") + .endDate("2023-12-25") + .roomId(1L) + .build(), + ValidatePreoccupyRoomResponseDto.builder() + .cartId(2L) + .roomCode(1002L) + .startDate("2023-11-11") + .endDate("2023-11-14") + .roomId(3L) + .build() + ) + ) + .build(); given(securityUtil.getCurrentMemberId()) .willReturn(1L); - willDoNothing() - .given(preoccupyRoomsLockFacade) - .checkAvailableAndPreoccupy(1L, requestDto); + given(preoccupyRoomsLockFacade.checkAvailableAndPreoccupy(1L, requestDto)) + .willReturn(responseDto); // when mockMvc.perform(post(requestUrl) @@ -307,9 +371,12 @@ void checkAvailableAndPreoccupy() throws Exception { fieldWithPath("rooms").type(JsonFieldType.ARRAY).description("예약할 객실 리스트") .attributes(key("constraints").value( preoccupyRoomsDescriptions.descriptionsForProperty("rooms"))), - fieldWithPath("rooms[].roomId").type(JsonFieldType.NUMBER).description("예약할 객실 식별자") + fieldWithPath("rooms[].cartId").type(JsonFieldType.NUMBER).description("장바구니 식별자") + .attributes(key("constraints").value( + preoccupyRoomItemDescriptions.descriptionsForProperty("cartId"))), + fieldWithPath("rooms[].roomCode").type(JsonFieldType.NUMBER).description("예약할 객실 코드") .attributes(key("constraints").value( - preoccupyRoomItemDescriptions.descriptionsForProperty("roomId"))), + preoccupyRoomItemDescriptions.descriptionsForProperty("roomCode"))), fieldWithPath("rooms[].startDate").type(JsonFieldType.STRING).description("숙박 시작일") .attributes(key("constraints").value( preoccupyRoomItemDescriptions.descriptionsForProperty("startDate"))), @@ -318,7 +385,14 @@ void checkAvailableAndPreoccupy() throws Exception { preoccupyRoomItemDescriptions.descriptionsForProperty("endDate"))) ), responseFields(responseCommon()).and( - fieldWithPath("data").type(JsonFieldType.NULL).description("응답 데이터") + fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), + fieldWithPath("data.isAvailable").type(JsonFieldType.BOOLEAN).description("예약 선점 가능 여부"), + fieldWithPath("data.roomResults").type(JsonFieldType.ARRAY).description("객실별 예약 선점 가능 검증 결과 리스트"), + fieldWithPath("data.roomResults[].cartId").type(JsonFieldType.NUMBER).description("장바구니 식별자"), + fieldWithPath("data.roomResults[].roomCode").type(JsonFieldType.NUMBER).description("객실 코드"), + fieldWithPath("data.roomResults[].startDate").type(JsonFieldType.STRING).description("숙박 시작일"), + fieldWithPath("data.roomResults[].endDate").type(JsonFieldType.STRING).description("숙박 마지막일"), + fieldWithPath("data.roomResults[].roomId").type(JsonFieldType.NUMBER).description("매칭된 객실 식별자 (-1인 경우, 예약 선점 불가능한 객실)") ) ) ); @@ -333,13 +407,22 @@ void releaseRooms() throws Exception { // given String requestUrl = "/api/reservations/release"; - ReleaseRoomsRequestDto requestDto = new ReleaseRoomsRequestDto( - List.of( - new ReleaseRoomItemRequestDto(1L, "2023-12-23", "2023-12-25"), - new ReleaseRoomItemRequestDto(2L, "2023-11-11", "2023-11-14"), - new ReleaseRoomItemRequestDto(3L, "2023-11-15", "2023-11-16") - ) - ); + ReleaseRoomsRequestDto requestDto = + ReleaseRoomsRequestDto.builder() + .rooms( + List.of( + ReleaseRoomItemRequestDto.builder() + .roomId(1L).startDate("2023-12-23").endDate("2023-12-25") + .build(), + ReleaseRoomItemRequestDto.builder() + .roomId(2L).startDate("2023-11-11").endDate("2023-11-14") + .build(), + ReleaseRoomItemRequestDto.builder() + .roomId(3L).startDate("2023-11-15").endDate("2023-11-16") + .build() + ) + ) + .build(); given(securityUtil.getCurrentMemberId()) .willReturn(1L); diff --git a/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/controller/ReservationRestControllerTest.java b/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/controller/ReservationRestControllerTest.java index cf317db2..7fd17cf2 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/controller/ReservationRestControllerTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/controller/ReservationRestControllerTest.java @@ -5,12 +5,14 @@ import com.fc.shimpyo_be.domain.reservation.dto.request.*; import com.fc.shimpyo_be.domain.reservation.dto.response.ReservationInfoResponseDto; import com.fc.shimpyo_be.domain.reservation.dto.response.SaveReservationResponseDto; -import com.fc.shimpyo_be.domain.reservation.dto.response.ValidationResultResponseDto; +import com.fc.shimpyo_be.domain.reservation.dto.response.ValidatePreoccupyResultResponseDto; +import com.fc.shimpyo_be.domain.reservation.dto.response.ValidatePreoccupyRoomResponseDto; import com.fc.shimpyo_be.domain.reservation.entity.PayMethod; import com.fc.shimpyo_be.domain.reservation.facade.PreoccupyRoomsLockFacade; import com.fc.shimpyo_be.domain.reservation.facade.ReservationLockFacade; import com.fc.shimpyo_be.domain.reservation.service.ReservationService; import com.fc.shimpyo_be.domain.reservationproduct.dto.request.ReservationProductRequestDto; +import com.fc.shimpyo_be.domain.reservationproduct.dto.response.ReservationProductResponseDto; import com.fc.shimpyo_be.global.util.SecurityUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -27,10 +29,9 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import java.util.ArrayList; import java.util.List; -import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; @@ -78,18 +79,72 @@ void saveReservation_Api_test() throws Exception { String requestUrl = "/api/reservations"; SaveReservationRequestDto requestDto - = new SaveReservationRequestDto( - List.of( - getReservationProductRequestData( - 1L, "2023-11-20", "2023-11-23", - "visitor1", "010-1111-1111", 150000), - getReservationProductRequestData( - 2L, "2023-11-18", "2023-11-20", - "visitor2", "010-2222-2222", 200000) - ), PayMethod.CREDIT_CARD, 350000 - ); + = SaveReservationRequestDto.builder() + .reservationProducts( + List.of( + ReservationProductRequestDto.builder() + .cartId(1L) + .roomId(1L) + .startDate("2023-11-20") + .endDate("2023-11-23") + .visitorName("visitor1") + .visitorPhone("010-1111-1111") + .price(300000) + .build(), + ReservationProductRequestDto.builder() + .cartId(2L) + .roomId(3L) + .startDate("2023-12-10") + .endDate("2023-12-12") + .visitorName("visitor2") + .visitorPhone("010-2222-2222") + .price(150000) + .build() + ) + ) + .payMethod(PayMethod.CREDIT_CARD) + .totalPrice(450000) + .build(); - SaveReservationResponseDto responseDto = new SaveReservationResponseDto(1L, requestDto); + SaveReservationResponseDto responseDto = + SaveReservationResponseDto.builder() + .reservationId(1L) + .reservationProducts( + List.of( + ReservationProductResponseDto.builder() + .productName("숙소1") + .roomId(1L) + .roomName("객실1") + .standard(2) + .capacity(3) + .startDate("2023-11-20") + .endDate("2023-11-23") + .checkIn("13:00") + .checkOut("12:00") + .visitorName("visitor1") + .visitorPhone("010-1111-1111") + .price(300000) + .build(), + ReservationProductResponseDto.builder() + .productName("숙소2") + .roomId(3L) + .roomName("객실3") + .standard(2) + .capacity(3) + .startDate("2023-12-10") + .endDate("2023-12-12") + .checkIn("13:00") + .checkOut("12:00") + .visitorName("visitor2") + .visitorPhone("010-2222-2222") + .price(150000) + .build() + ) + ) + .payMethod(requestDto.payMethod()) + .totalPrice(requestDto.totalPrice()) + .createdAt("2023-12-06 10:30:35") + .build(); given(securityUtil.getCurrentMemberId()).willReturn(1L); given(reservationLockFacade.saveReservation(anyLong(), any(SaveReservationRequestDto.class))) @@ -118,22 +173,24 @@ void getReservationInfoList_Api_test() throws Exception { PageRequest pageRequest = PageRequest.of(page, size); List content = List.of( - new ReservationInfoResponseDto( - 2L, - 3L, - 5L, - "호텔2", - "호텔 photoUrl", - "호텔 주소 url", - 1L, - "객실1", - "2023-11-23", - "2023-11-26", - "14:00", - "12:00", - 220000, - "CREDIT_CARD" - ) + ReservationInfoResponseDto.builder() + .reservationId(2L) + .reservationProductId(3L) + .productId(5L) + .productName("호텔1") + .productImageUrl("호텔1 photo URL") + .productAddress("호텔1 주소") + .productDetailAddress("호텔 상세 주소") + .roomId(1L) + .roomName("객실1") + .startDate("2023-11-23") + .endDate("2023-11-26") + .checkIn("14:00") + .checkOut("12:00") + .price(220000) + .payMethod("CREDIT_CARD") + .createdAt("2023-11-20 10:00:00") + .build() ); given(securityUtil.getCurrentMemberId()).willReturn(1L); @@ -158,31 +215,62 @@ void checkAvailableAndPreoccupy_test() throws Exception { // given String requestUrl = "/api/reservations/preoccupy"; - PreoccupyRoomsRequestDto requestDto - = new PreoccupyRoomsRequestDto( - List.of( - new PreoccupyRoomItemRequestDto(1L, "2023-12-23", "2023-12-25"), - new PreoccupyRoomItemRequestDto(2L, "2023-11-11", "2023-11-14") + PreoccupyRoomsRequestDto requestDto = + PreoccupyRoomsRequestDto.builder() + .rooms( + List.of( + PreoccupyRoomItemRequestDto.builder() + .cartId(1L) + .roomCode(1001L) + .startDate("2023-12-23") + .endDate("2023-12-25") + .build(), + PreoccupyRoomItemRequestDto.builder() + .cartId(2L) + .roomCode(1003L) + .startDate("2023-11-11") + .endDate("2023-11-14") + .build() + ) ) - ); - - ValidationResultResponseDto responseDto - = new ValidationResultResponseDto(true, new ArrayList<>()); + .build(); + + ValidatePreoccupyResultResponseDto responseDto = + ValidatePreoccupyResultResponseDto.builder() + .isAvailable(true) + .roomResults( + List.of( + ValidatePreoccupyRoomResponseDto.builder() + .cartId(1L) + .roomCode(1001L) + .startDate("2023-12-23") + .endDate("2023-12-25") + .roomId(1L) + .build(), + ValidatePreoccupyRoomResponseDto.builder() + .cartId(2L) + .roomCode(1003L) + .startDate("2023-11-11") + .endDate("2023-11-14") + .roomId(3L) + .build() + ) + ) + .build(); given(securityUtil.getCurrentMemberId()) .willReturn(1L); - willDoNothing() - .given(preoccupyRoomsLockFacade) - .checkAvailableAndPreoccupy(1L, requestDto); + given(preoccupyRoomsLockFacade.checkAvailableAndPreoccupy(1L, requestDto)) + .willReturn(responseDto); // when & then mockMvc.perform( - post(requestUrl) - .content(objectMapper.writeValueAsString(requestDto)) - .contentType(MediaType.APPLICATION_JSON) - ) + post(requestUrl) + .content(objectMapper.writeValueAsString(requestDto)) + .contentType(MediaType.APPLICATION_JSON) + ) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data").isEmpty()); + .andExpect(jsonPath("$.data.isAvailable", is(true))); } @WithMockUser(roles = "USER") @@ -192,24 +280,28 @@ void checkAvailableAndPreoccupy_size_validation_error_test() throws Exception { // given String requestUrl = "/api/reservations/preoccupy"; - PreoccupyRoomsRequestDto requestDto - = new PreoccupyRoomsRequestDto( - List.of( - new PreoccupyRoomItemRequestDto(1L, "2023-12-23", "2023-12-25"), - new PreoccupyRoomItemRequestDto(2L, "2023-11-11", "2023-11-14"), - new PreoccupyRoomItemRequestDto(3L, "2023-11-11", "2023-11-14"), - new PreoccupyRoomItemRequestDto(4L, "2023-11-11", "2023-11-14") - ) - ); - - ValidationResultResponseDto responseDto - = new ValidationResultResponseDto(true, new ArrayList<>()); + PreoccupyRoomsRequestDto requestDto = + PreoccupyRoomsRequestDto.builder() + .rooms( + List.of( + PreoccupyRoomItemRequestDto.builder() + .cartId(1L).roomCode(1001L).startDate("2023-12-23").endDate("2023-12-25") + .build(), + PreoccupyRoomItemRequestDto.builder() + .cartId(2L).roomCode(1002L).startDate("2023-11-11").endDate("2023-11-14") + .build(), + PreoccupyRoomItemRequestDto.builder() + .cartId(3L).roomCode(1003L).startDate("2023-11-11").endDate("2023-11-14") + .build(), + PreoccupyRoomItemRequestDto.builder() + .cartId(4L).roomCode(1004L).startDate("2023-11-16").endDate("2023-11-18") + .build() + ) + ) + .build(); given(securityUtil.getCurrentMemberId()) .willReturn(1L); - willDoNothing() - .given(preoccupyRoomsLockFacade) - .checkAvailableAndPreoccupy(1L, requestDto); // when & then mockMvc.perform( @@ -228,23 +320,25 @@ void checkAvailableAndPreoccupy_localdate_validation_error_test() throws Excepti // given String requestUrl = "/api/reservations/preoccupy"; - PreoccupyRoomsRequestDto requestDto - = new PreoccupyRoomsRequestDto( - List.of( - new PreoccupyRoomItemRequestDto(1L, "202-12-23", "2023-12-25"), - new PreoccupyRoomItemRequestDto(2L, "2023-11-11", "2023-11-14"), - new PreoccupyRoomItemRequestDto(3L, "2023-11-11", "2023-11-14") - ) - ); - - ValidationResultResponseDto responseDto - = new ValidationResultResponseDto(true, new ArrayList<>()); + PreoccupyRoomsRequestDto requestDto = + PreoccupyRoomsRequestDto.builder() + .rooms( + List.of( + PreoccupyRoomItemRequestDto.builder() + .cartId(1L).roomCode(1001L).startDate("202-12-23").endDate("2023-12-25") + .build(), + PreoccupyRoomItemRequestDto.builder() + .cartId(2L).roomCode(1002L).startDate("2023-11-11").endDate("2023-11-14") + .build(), + PreoccupyRoomItemRequestDto.builder() + .cartId(3L).roomCode(1003L).startDate("2023-11-11").endDate("2023-11-14") + .build() + ) + ) + .build(); given(securityUtil.getCurrentMemberId()) .willReturn(1L); - willDoNothing() - .given(preoccupyRoomsLockFacade) - .checkAvailableAndPreoccupy(1L, requestDto); // when & then mockMvc.perform( @@ -263,13 +357,22 @@ void releaseRooms_test() throws Exception { // given String requestUrl = "/api/reservations/release"; - ReleaseRoomsRequestDto requestDto = new ReleaseRoomsRequestDto( - List.of( - new ReleaseRoomItemRequestDto(1L, "2023-12-23", "2023-12-25"), - new ReleaseRoomItemRequestDto(2L, "2023-11-11", "2023-11-14"), - new ReleaseRoomItemRequestDto(3L, "2023-11-15", "2023-11-16") - ) - ); + ReleaseRoomsRequestDto requestDto = + ReleaseRoomsRequestDto.builder() + .rooms( + List.of( + ReleaseRoomItemRequestDto.builder() + .roomId(1L).startDate("2023-12-23").endDate("2023-12-25") + .build(), + ReleaseRoomItemRequestDto.builder() + .roomId(2L).startDate("2023-11-11").endDate("2023-11-14") + .build(), + ReleaseRoomItemRequestDto.builder() + .roomId(3L).startDate("2023-11-15").endDate("2023-11-16") + .build() + ) + ) + .build(); given(securityUtil.getCurrentMemberId()) .willReturn(1L); @@ -287,29 +390,4 @@ void releaseRooms_test() throws Exception { .andExpect(jsonPath("$.code", is(200))) .andExpect(jsonPath("$.data").isEmpty()); } - - private ReservationProductRequestDto getReservationProductRequestData( - long roomId, - String startDate, - String endDate, - String visitorName, - String visitorPhone, - Integer price - ) { - String defaultValue = "DEFAULT_VALUE"; - return new ReservationProductRequestDto( - roomId, - defaultValue, - defaultValue, - 2, - 4, - startDate, - endDate, - "13:00", - "12:00", - visitorName, - visitorPhone, - price - ); - } } diff --git a/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/facade/PreoccupyRoomsLockFacadeTest.java b/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/facade/PreoccupyRoomsLockFacadeTest.java index 4a3b6df2..64c25aac 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/facade/PreoccupyRoomsLockFacadeTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/facade/PreoccupyRoomsLockFacadeTest.java @@ -1,29 +1,191 @@ package com.fc.shimpyo_be.domain.reservation.unit.facade; import com.fc.shimpyo_be.config.AbstractContainersSupport; +import com.fc.shimpyo_be.config.DatabaseCleanUp; +import com.fc.shimpyo_be.config.TestDBCleanerConfig; +import com.fc.shimpyo_be.domain.product.entity.*; +import com.fc.shimpyo_be.domain.product.repository.ProductRepository; import com.fc.shimpyo_be.domain.reservation.dto.request.PreoccupyRoomItemRequestDto; import com.fc.shimpyo_be.domain.reservation.dto.request.PreoccupyRoomsRequestDto; import com.fc.shimpyo_be.domain.reservation.facade.PreoccupyRoomsLockFacade; +import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.domain.room.entity.RoomOption; +import com.fc.shimpyo_be.domain.room.entity.RoomPrice; +import com.fc.shimpyo_be.domain.room.repository.RoomRepository; import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; import org.springframework.util.StopWatch; +import java.time.LocalTime; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ThreadLocalRandom; @Slf4j +@Import(TestDBCleanerConfig.class) @SpringBootTest public class PreoccupyRoomsLockFacadeTest extends AbstractContainersSupport { @Autowired private PreoccupyRoomsLockFacade preoccupyRoomsLockFacade; + @Autowired + private RoomRepository roomRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + private final String[] tableNameArray + = {"product", "product_option", "address", "room", "room_option", "amenity", "room_price"}; + private static final ThreadLocal threadLocalStopWatch = ThreadLocal.withInitial(StopWatch::new); + @BeforeEach + void setUp() { + databaseCleanUp.cleanUp(tableNameArray); + + List products = new ArrayList<>(); + for (int i = 1; i <= 3; i++) { + String productName = "호텔" + i; + float starAvg = ThreadLocalRandom.current().nextFloat(0, 5); + String infoCenter = String.format("02-1234-%d%d%d%d", i, i, i, i); + products.add( + productRepository.save( + Product.builder() + .name(productName) + .thumbnail(productName + " 썸네일 url") + .description(productName + " 설명") + .starAvg(starAvg) + .category(Category.TOURIST_HOTEL) + .address( + Address.builder() + .address(productName + " 주소") + .detailAddress(productName + " 상세 주소") + .mapX(1.0) + .mapY(1.5) + .build() + ) + .productOption( + ProductOption.builder() + .cooking(true) + .foodPlace("음료 가능") + .parking(true) + .pickup(false) + .infoCenter(infoCenter) + .build() + ) + .amenity( + Amenity.builder() + .barbecue(false) + .beauty(true) + .beverage(true) + .fitness(true) + .bicycle(false) + .campfire(false) + .karaoke(true) + .publicBath(true) + .publicPc(true) + .seminar(false) + .sports(false) + .build() + ) + .build() + ) + ); + } + + for (int i = 1; i <= 3; i++) { + String roomName = "객실" + i; + roomRepository.save( + Room.builder() + .product(products.get(i - 1)) + .name(roomName) + .code(1000 + i) + .description(roomName + " 설명") + .standard(2) + .capacity(4) + .checkIn(LocalTime.of(14, 0)) + .checkOut(LocalTime.of(12, 0)) + .price( + RoomPrice.builder() + .offWeekDaysMinFee(75000) + .offWeekendMinFee(85000) + .peakWeekDaysMinFee(100000) + .peakWeekendMinFee(120000) + .build() + ) + .roomOption( + RoomOption.builder() + .cooking(true) + .airCondition(true) + .bath(true) + .bathFacility(true) + .pc(false) + .diningTable(true) + .hairDryer(true) + .homeTheater(false) + .internet(true) + .cable(false) + .refrigerator(true) + .sofa(true) + .toiletries(true) + .tv(true) + .build() + ) + .build() + ); + } + + roomRepository.save( + Room.builder() + .product(products.get(0)) + .name("객실1") + .code(1001) + .description("객실1 설명") + .standard(2) + .capacity(4) + .checkIn(LocalTime.of(14, 0)) + .checkOut(LocalTime.of(12, 0)) + .price( + RoomPrice.builder() + .offWeekDaysMinFee(75000) + .offWeekendMinFee(85000) + .peakWeekDaysMinFee(100000) + .peakWeekendMinFee(120000) + .build() + ) + .roomOption( + RoomOption.builder() + .cooking(true) + .airCondition(true) + .bath(true) + .bathFacility(true) + .pc(false) + .diningTable(true) + .hairDryer(true) + .homeTheater(false) + .internet(true) + .cable(false) + .refrigerator(true) + .sofa(true) + .toiletries(true) + .tv(true) + .build() + ) + .build() + ); + } + @Test void checkAvailable() throws InterruptedException { int poolSize = 2; @@ -32,26 +194,90 @@ void checkAvailable() throws InterruptedException { ExecutorService executors = Executors.newFixedThreadPool(poolSize); CountDownLatch latch = new CountDownLatch(threadSize); - PreoccupyRoomsRequestDto request1 = new PreoccupyRoomsRequestDto(List.of( - new PreoccupyRoomItemRequestDto(1L, "2024-03-05", "2024-03-08"), - new PreoccupyRoomItemRequestDto(2L, "2024-04-05", "2024-04-09") - )); - PreoccupyRoomsRequestDto request2 = new PreoccupyRoomsRequestDto(List.of( - new PreoccupyRoomItemRequestDto(1L, "2024-03-06", "2024-03-09"), - new PreoccupyRoomItemRequestDto(2L, "2024-04-04", "2024-04-06") - )); - PreoccupyRoomsRequestDto request3 = new PreoccupyRoomsRequestDto(List.of( - new PreoccupyRoomItemRequestDto(1L, "2024-02-10", "2024-02-15"), - new PreoccupyRoomItemRequestDto(3L, "2024-04-05", "2024-04-09") - )); - PreoccupyRoomsRequestDto request4 = new PreoccupyRoomsRequestDto(List.of( - new PreoccupyRoomItemRequestDto(3L, "2024-04-04", "2024-04-08") - )); - PreoccupyRoomsRequestDto request5 = new PreoccupyRoomsRequestDto(List.of( - new PreoccupyRoomItemRequestDto(5L, "2024-03-05", "2024-03-08"), - new PreoccupyRoomItemRequestDto(6L, "2024-04-05", "2024-04-09"), - new PreoccupyRoomItemRequestDto(7L, "2024-04-05", "2024-04-09") - )); + PreoccupyRoomsRequestDto request1 = PreoccupyRoomsRequestDto.builder() + .rooms( + List.of( + PreoccupyRoomItemRequestDto.builder() + .roomCode(1001L) + .startDate("2024-03-06") + .endDate("2024-03-09") + .build(), + PreoccupyRoomItemRequestDto.builder() + .roomCode(1002L) + .startDate("2024-04-04") + .endDate("2024-04-06") + .build() + ) + ) + .build(); + + PreoccupyRoomsRequestDto request2 = PreoccupyRoomsRequestDto.builder() + .rooms( + List.of( + PreoccupyRoomItemRequestDto.builder() + .roomCode(1001L) + .startDate("2024-03-05") + .endDate("2024-03-08") + .build(), + PreoccupyRoomItemRequestDto.builder() + .roomCode(1001L) + .startDate("2024-03-05") + .endDate("2024-03-08") + .build() + ) + ) + .build(); + + PreoccupyRoomsRequestDto request3 = PreoccupyRoomsRequestDto.builder() + .rooms( + List.of( + PreoccupyRoomItemRequestDto.builder() + .roomCode(1001L) + .startDate("2024-02-10") + .endDate("2024-02-15") + .build(), + PreoccupyRoomItemRequestDto.builder() + .roomCode(1003L) + .startDate("2024-04-05") + .endDate("2024-04-09") + .build() + ) + ) + .build(); + + PreoccupyRoomsRequestDto request4 = PreoccupyRoomsRequestDto.builder() + .rooms( + List.of( + PreoccupyRoomItemRequestDto.builder() + .roomCode(1003L) + .startDate("2024-04-04") + .endDate("2024-04-08") + .build() + ) + ) + .build(); + + PreoccupyRoomsRequestDto request5 = PreoccupyRoomsRequestDto.builder() + .rooms( + List.of( + PreoccupyRoomItemRequestDto.builder() + .roomCode(1001L) + .startDate("2024-02-10") + .endDate("2024-02-15") + .build(), + PreoccupyRoomItemRequestDto.builder() + .roomCode(1003L) + .startDate("2024-02-05") + .endDate("2024-02-09") + .build(), + PreoccupyRoomItemRequestDto.builder() + .roomCode(1002L) + .startDate("2024-05-10") + .endDate("2024-05-12") + .build() + ) + ) + .build(); executors.submit(() -> { StopWatch stopWatch = threadLocalStopWatch.get(); @@ -59,7 +285,7 @@ void checkAvailable() throws InterruptedException { stopWatch.start(); preoccupyRoomsLockFacade.checkAvailableAndPreoccupy(1L, request1); } catch (Exception e) { - System.out.println(e.getMessage()); + log.error(e.getMessage()); } finally { stopWatch.stop(); log.info("{} ::: {} ms", "request1", stopWatch.getTotalTimeMillis()); @@ -74,7 +300,7 @@ void checkAvailable() throws InterruptedException { stopWatch.start(); preoccupyRoomsLockFacade.checkAvailableAndPreoccupy(2L, request2); } catch (Exception e) { - System.out.println(e.getMessage()); + log.error(e.getMessage()); } finally { stopWatch.stop(); log.info("{} ::: {} ms", "request2", stopWatch.getTotalTimeMillis()); @@ -89,7 +315,7 @@ void checkAvailable() throws InterruptedException { stopWatch.start(); preoccupyRoomsLockFacade.checkAvailableAndPreoccupy(3L, request3); } catch (Exception e) { - System.out.println(e.getMessage()); + log.error(e.getMessage()); } finally { stopWatch.stop(); log.info("{} ::: {} ms", "request3", stopWatch.getTotalTimeMillis()); @@ -104,7 +330,7 @@ void checkAvailable() throws InterruptedException { stopWatch.start(); preoccupyRoomsLockFacade.checkAvailableAndPreoccupy(4L, request4); } catch (Exception e) { - System.out.println(e.getMessage()); + log.error(e.getMessage()); } finally { stopWatch.stop(); log.info("{} ::: {} ms", "request4", stopWatch.getTotalTimeMillis()); @@ -119,7 +345,7 @@ void checkAvailable() throws InterruptedException { stopWatch.start(); preoccupyRoomsLockFacade.checkAvailableAndPreoccupy(5L, request5); } catch (Exception e) { - System.out.println(e.getMessage()); + log.error(e.getMessage()); } finally { stopWatch.stop(); log.info("{} ::: {} ms", "request5", stopWatch.getTotalTimeMillis()); diff --git a/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/facade/ReservationLockFacadeTest.java b/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/facade/ReservationLockFacadeTest.java index 458b60ee..5aaea371 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/facade/ReservationLockFacadeTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/facade/ReservationLockFacadeTest.java @@ -1,27 +1,39 @@ package com.fc.shimpyo_be.domain.reservation.unit.facade; import com.fc.shimpyo_be.config.AbstractContainersSupport; +import com.fc.shimpyo_be.config.DatabaseCleanUp; +import com.fc.shimpyo_be.config.TestDBCleanerConfig; +import com.fc.shimpyo_be.domain.member.entity.Authority; +import com.fc.shimpyo_be.domain.member.entity.Member; +import com.fc.shimpyo_be.domain.member.repository.MemberRepository; +import com.fc.shimpyo_be.domain.product.entity.*; +import com.fc.shimpyo_be.domain.product.repository.ProductRepository; import com.fc.shimpyo_be.domain.reservation.dto.request.SaveReservationRequestDto; import com.fc.shimpyo_be.domain.reservation.entity.PayMethod; import com.fc.shimpyo_be.domain.reservation.facade.ReservationLockFacade; import com.fc.shimpyo_be.domain.reservationproduct.dto.request.ReservationProductRequestDto; +import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.domain.room.entity.RoomOption; +import com.fc.shimpyo_be.domain.room.entity.RoomPrice; +import com.fc.shimpyo_be.domain.room.repository.RoomRepository; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.util.StopWatch; import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; @Slf4j +@Import(TestDBCleanerConfig.class) @SpringBootTest class ReservationLockFacadeTest extends AbstractContainersSupport { @@ -31,6 +43,22 @@ class ReservationLockFacadeTest extends AbstractContainersSupport { @Autowired private RedisTemplate redisTemplate; + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private RoomRepository roomRepository; + + private final String[] tableNameArray = { + "member", "product", "room", "product_option", "address", "room_option", "amenity" + }; + private static final ThreadLocal threadLocalStopWatch = ThreadLocal.withInitial(StopWatch::new); @BeforeEach @@ -41,6 +69,115 @@ void setUp() { long memberId4 = 4L; long memberId5 = 5L; + databaseCleanUp.cleanUp(tableNameArray); + + for (int i = 1; i <= 5; i++) { + String name = "member" + i; + memberRepository.save( + Member.builder() + .email(name + "@email.com") + .name(name) + .password("password") + .photoUrl("member photo url") + .authority(Authority.ROLE_USER) + .build() + ); + } + + List products = new ArrayList<>(); + for (int i = 1; i <= 3; i++) { + String productName = "호텔" + i; + float starAvg = ThreadLocalRandom.current().nextFloat(0, 5); + String infoCenter = String.format("02-1234-%d%d%d%d", i, i, i, i); + products.add( + productRepository.save( + Product.builder() + .name(productName) + .thumbnail(productName + " 썸네일 url") + .description(productName + " 설명") + .starAvg(starAvg) + .category(Category.TOURIST_HOTEL) + .address( + Address.builder() + .address(productName + " 주소") + .detailAddress(productName + " 상세 주소") + .mapX(1.0) + .mapY(1.5) + .build() + ) + .productOption( + ProductOption.builder() + .cooking(true) + .foodPlace("음료 가능") + .parking(true) + .pickup(false) + .infoCenter(infoCenter) + .build() + ) + .amenity( + Amenity.builder() + .barbecue(false) + .beauty(true) + .beverage(true) + .fitness(true) + .bicycle(false) + .campfire(false) + .karaoke(true) + .publicBath(true) + .publicPc(true) + .seminar(false) + .sports(false) + .build() + ) + .build() + ) + ); + } + + List rooms = new ArrayList<>(); + for (int i = 1; i <= 5; i++) { + String roomName = "객실" + i; + rooms.add( + roomRepository.save( + Room.builder() + .product(products.get((i - 1) % 3)) + .name(roomName) + .description(roomName + " 설명") + .standard(2) + .capacity(4) + .checkIn(LocalTime.of(14, 0)) + .checkOut(LocalTime.of(12, 0)) + .price( + RoomPrice.builder() + .offWeekDaysMinFee(75000) + .offWeekendMinFee(85000) + .peakWeekDaysMinFee(100000) + .peakWeekendMinFee(120000) + .build() + ) + .roomOption( + RoomOption.builder() + .cooking(true) + .airCondition(true) + .bath(true) + .bathFacility(true) + .pc(false) + .diningTable(true) + .hairDryer(true) + .homeTheater(false) + .internet(true) + .cable(false) + .refrigerator(true) + .sofa(true) + .toiletries(true) + .tv(true) + .build() + ) + .build() + ) + ); + } + saveRedisData(memberId1, 2L, LocalDate.of(2023, 11, 20), LocalDate.of(2023, 11, 23)); saveRedisData(memberId1, 3L, LocalDate.of(2023, 11, 24), LocalDate.of(2023, 11, 26)); @@ -68,36 +205,47 @@ void saveReservation_test() throws InterruptedException { SaveReservationRequestDto request1 = new SaveReservationRequestDto( List.of( - getReservationProductRequestData(2L, "2023-11-20", "2023-11-23"), - getReservationProductRequestData(3L, "2023-11-24", "2023-11-26") + new ReservationProductRequestDto(1L, 2L, "2023-11-20", "2023-11-23", + "visitor1", "010-1111-1111", 100000), + new ReservationProductRequestDto(2L, 3L, "2023-11-24", "2023-11-26", + "visitor2", "010-2222-2222", 150000) ), PayMethod.KAKAO_PAY, 250000 ); SaveReservationRequestDto request2 = new SaveReservationRequestDto( - List.of(getReservationProductRequestData(4L, "2023-10-10", "2023-10-14")), + List.of(new ReservationProductRequestDto(3L, 4L, "2023-10-10", "2023-10-14", + "visitor3", "010-3333-3333", 125000)), PayMethod.CREDIT_CARD, 125000 ); SaveReservationRequestDto request3 = new SaveReservationRequestDto( List.of( - getReservationProductRequestData(2L, "2023-11-10", "2023-11-14"), - getReservationProductRequestData(5L, "2023-10-17", "2023-10-20"), - getReservationProductRequestData(3L, "2023-10-10", "2023-10-14") + new ReservationProductRequestDto(4L, 2L, "2023-11-10", "2023-11-14", + "visitor", "010-1111-1111", 90000), + new ReservationProductRequestDto(5L, 5L, "2023-10-17", "2023-10-20", + "visitor", "010-1111-1111", 110000), + new ReservationProductRequestDto(6L, 3L, "2023-10-10", "2023-10-14", + "visitor", "010-1111-1111", 100000) ), PayMethod.NAVER_PAY, 300000 ); SaveReservationRequestDto request4 = new SaveReservationRequestDto( List.of( - getReservationProductRequestData(4L, "2023-10-05", "2023-10-07"), - getReservationProductRequestData(5L, "2023-10-10", "2023-10-14") - ), PayMethod.KAKAO_PAY, 250000 + new ReservationProductRequestDto(7L, 4L, "2023-10-05", "2023-10-07", + "visitor", "010-1111-1111", 125000), + new ReservationProductRequestDto(8L, 5L, "2023-10-10", "2023-10-14", + "visitor", "010-1111-1111", 125000) + ), PayMethod.KAKAO_PAY, 250000 ); SaveReservationRequestDto request5 = new SaveReservationRequestDto( List.of( - getReservationProductRequestData(1L, "2023-10-10", "2023-10-14"), - getReservationProductRequestData(2L, "2023-12-11", "2023-12-15"), - getReservationProductRequestData(4L, "2023-11-11", "2023-11-14") + new ReservationProductRequestDto(9L, 1L, "2023-10-10", "2023-10-14", + "visitor4", "010-4444-4444", 150000), + new ReservationProductRequestDto(10L, 2L, "2023-12-11", "2023-12-15", + "visitor4", "010-4444-4444", 150000), + new ReservationProductRequestDto(11L, 4L, "2023-11-11", "2023-11-14", + "visitor4", "010-4444-4444", 100000) ), PayMethod.PAYPAL, 400000 ); @@ -185,26 +333,8 @@ private void saveRedisData(long memberId, long roomId, LocalDate start, LocalDat while (start.isBefore(end)) { String key = String.format(keyFormat, roomId, start); - valueOperations.set(key, String.valueOf(memberId), 50, TimeUnit.SECONDS); + valueOperations.set(key, String.valueOf(memberId), 60, TimeUnit.SECONDS); start = start.plusDays(1); } } - - private ReservationProductRequestDto getReservationProductRequestData(long roomId, String startDate, String endDate) { - String defaultValue = "DEFAULT_VALUE"; - return new ReservationProductRequestDto( - roomId, - defaultValue, - defaultValue, - 2, - 4, - startDate, - endDate, - defaultValue, - defaultValue, - defaultValue, - defaultValue, - 100000 - ); - } } diff --git a/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/repository/ReservationRepositoryTest.java b/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/repository/ReservationRepositoryTest.java index 065f87f6..cb070e90 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/repository/ReservationRepositoryTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/repository/ReservationRepositoryTest.java @@ -1,34 +1,200 @@ package com.fc.shimpyo_be.domain.reservation.unit.repository; +import com.fc.shimpyo_be.config.DatabaseCleanUp; +import com.fc.shimpyo_be.config.TestDBCleanerConfig; import com.fc.shimpyo_be.config.TestQuerydslConfig; +import com.fc.shimpyo_be.domain.member.entity.Authority; +import com.fc.shimpyo_be.domain.member.entity.Member; +import com.fc.shimpyo_be.domain.member.repository.MemberRepository; +import com.fc.shimpyo_be.domain.product.entity.*; +import com.fc.shimpyo_be.domain.product.repository.ProductRepository; +import com.fc.shimpyo_be.domain.reservation.entity.PayMethod; +import com.fc.shimpyo_be.domain.reservation.entity.Reservation; import com.fc.shimpyo_be.domain.reservation.repository.ReservationRepository; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import com.fc.shimpyo_be.domain.reservationproduct.entity.ReservationProduct; +import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.domain.room.entity.RoomOption; +import com.fc.shimpyo_be.domain.room.entity.RoomPrice; +import com.fc.shimpyo_be.domain.room.repository.RoomRepository; +import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import org.springframework.test.context.jdbc.Sql; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; - -@Import(TestQuerydslConfig.class) +@Import({TestQuerydslConfig.class, TestDBCleanerConfig.class}) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @DataJpaTest public class ReservationRepositoryTest { + @Autowired + private DatabaseCleanUp databaseCleanUp; + @Autowired private ReservationRepository reservationRepository; - @Sql("classpath:testdata/reservation-repository-setup.sql") + @Autowired + private MemberRepository memberRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private RoomRepository roomRepository; + + private final String[] tableNameArray = { + "member", "product", "room", "product_option", + "address", "room_option", "amenity", "reservation_product", "reservation" + }; + + private Member member; + + @BeforeEach + void setUp() { + databaseCleanUp.cleanUp(tableNameArray); + + member = memberRepository.save( + Member.builder() + .email("member@email.com") + .name("member") + .password("password") + .photoUrl("member photo url") + .authority(Authority.ROLE_USER) + .build() + ); + + Product product = productRepository.save( + Product.builder() + .name("호텔1") + .thumbnail("호텔1 썸네일 url") + .description("호텔1 설명") + .starAvg(4.0f) + .category(Category.TOURIST_HOTEL) + .address( + Address.builder() + .address("호텔1 주소") + .detailAddress("호텔1 상세 주소") + .mapX(10000) + .mapY(11000) + .build() + ) + .productOption( + ProductOption.builder() + .cooking(true) + .foodPlace("음료 가능") + .parking(true) + .pickup(false) + .infoCenter("031-222-333") + .build() + ) + .amenity( + Amenity.builder() + .barbecue(false) + .beauty(true) + .beverage(true) + .fitness(true) + .bicycle(false) + .campfire(false) + .karaoke(true) + .publicBath(true) + .publicPc(true) + .seminar(false) + .sports(false) + .build() + ) + .build() + ); + + Room room = roomRepository.save( + Room.builder() + .product(product) + .name("객실1") + .description("객실1 설명") + .standard(2) + .capacity(4) + .checkIn(LocalTime.of(14, 0)) + .checkOut(LocalTime.of(12, 0)) + .price( + RoomPrice.builder() + .offWeekDaysMinFee(75000) + .offWeekendMinFee(85000) + .peakWeekDaysMinFee(100000) + .peakWeekendMinFee(120000) + .build() + ) + .roomOption( + RoomOption.builder() + .cooking(true) + .airCondition(true) + .bath(true) + .bathFacility(true) + .pc(false) + .diningTable(true) + .hairDryer(true) + .homeTheater(false) + .internet(true) + .cable(false) + .refrigerator(true) + .sofa(true) + .toiletries(true) + .tv(true) + .build() + ) + .build() + ); + + Reservation reservation1 = reservationRepository.save( + Reservation.builder() + .reservationProducts( + List.of( + ReservationProduct.builder() + .room(room) + .startDate(LocalDate.of(2023, 11, 10)) + .endDate(LocalDate.of(2023, 11, 12)) + .visitorName("방문자명") + .visitorPhone("010-1111-1111") + .price(200000) + .build() + ) + ) + .member(member) + .payMethod(PayMethod.CREDIT_CARD) + .totalPrice(200000) + .build() + ); + + Reservation reservation2 = reservationRepository.save( + Reservation.builder() + .reservationProducts( + List.of( + ReservationProduct.builder() + .room(room) + .startDate(LocalDate.of(2023, 12, 4)) + .endDate(LocalDate.of(2023, 12, 7)) + .visitorName("방문자명") + .visitorPhone("010-1111-1111") + .price(300000) + .build() + ) + ) + .totalPrice(300000) + .payMethod(PayMethod.KAKAO_PAY) + .member(member) + .build() + ); + } + @DisplayName("findIdsByMemberId 테스트") @Test void findIdsByMemberId_test() { //given - long memberId = 1; + long memberId = member.getId(); //when List result = reservationRepository.findIdsByMemberId(memberId); diff --git a/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/service/GetReservationListServiceTest.java b/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/service/GetReservationListServiceTest.java index 5dd03ee5..b87db936 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/service/GetReservationListServiceTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/service/GetReservationListServiceTest.java @@ -1,28 +1,43 @@ package com.fc.shimpyo_be.domain.reservation.unit.service; import com.fc.shimpyo_be.config.AbstractContainersSupport; +import com.fc.shimpyo_be.config.DatabaseCleanUp; +import com.fc.shimpyo_be.config.TestDBCleanerConfig; +import com.fc.shimpyo_be.domain.member.entity.Authority; +import com.fc.shimpyo_be.domain.member.entity.Member; +import com.fc.shimpyo_be.domain.member.repository.MemberRepository; +import com.fc.shimpyo_be.domain.product.entity.*; +import com.fc.shimpyo_be.domain.product.repository.ProductRepository; import com.fc.shimpyo_be.domain.reservation.dto.response.ReservationInfoResponseDto; +import com.fc.shimpyo_be.domain.reservation.entity.PayMethod; +import com.fc.shimpyo_be.domain.reservation.entity.Reservation; import com.fc.shimpyo_be.domain.reservation.repository.ReservationRepository; import com.fc.shimpyo_be.domain.reservation.service.ReservationService; -import com.fc.shimpyo_be.domain.reservationproduct.repository.ReservationProductRepository; +import com.fc.shimpyo_be.domain.reservationproduct.entity.ReservationProduct; +import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.domain.room.entity.RoomOption; +import com.fc.shimpyo_be.domain.room.entity.RoomPrice; +import com.fc.shimpyo_be.domain.room.repository.RoomRepository; import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.*; +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.core.io.ClassPathResource; +import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; -import org.springframework.jdbc.datasource.init.ScriptUtils; -import org.springframework.test.context.jdbc.Sql; -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.SQLException; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; @Slf4j -@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Import(TestDBCleanerConfig.class) @SpringBootTest public class GetReservationListServiceTest extends AbstractContainersSupport { @@ -33,36 +48,236 @@ public class GetReservationListServiceTest extends AbstractContainersSupport { private ReservationRepository reservationRepository; @Autowired - private ReservationProductRepository reservationProductRepository; + private MemberRepository memberRepository; - @BeforeAll - void setUp(@Autowired DataSource dataSource) throws SQLException { - try (Connection conn = dataSource.getConnection()) { - ScriptUtils - .executeSqlScript(conn, new ClassPathResource("testdata/reservation-service-setup.sql")); - } + @Autowired + private ProductRepository productRepository; + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + private Member member; + + private final String[] tableNameArray = { + "member", "product", "room", "product_option", + "address", "room_option", "amenity", "reservation_product", "reservation" + }; + + @BeforeEach + void setUp() { + databaseCleanUp.cleanUp(tableNameArray); + + member = memberRepository.save( + Member.builder() + .email("member@email.com") + .name("member") + .password("password") + .photoUrl("member photo url") + .authority(Authority.ROLE_USER) + .build() + ); + + List products = getProductTestDataList(3); + List rooms = getRoomTestDataList(5, products); + + Reservation reservation1 = reservationRepository.save( + Reservation.builder() + .reservationProducts( + List.of( + getReservationProduct( + rooms.get(0), + LocalDate.of(2023, 11, 10), + LocalDate.of(2023, 11, 12), + 200000 + ), + getReservationProduct( + rooms.get(1), + LocalDate.of(2023, 11, 20), + LocalDate.of(2023, 11, 22), + 200000 + ) + ) + ) + .member(member) + .payMethod(PayMethod.CREDIT_CARD) + .totalPrice(400000) + .build() + ); + + Reservation reservation2 = reservationRepository.save( + Reservation.builder() + .reservationProducts( + List.of( + getReservationProduct( + rooms.get(2), + LocalDate.of(2023, 12, 4), + LocalDate.of(2023, 12, 7), + 300000 + ) + ) + ) + .totalPrice(300000) + .payMethod(PayMethod.KAKAO_PAY) + .member(member) + .build() + ); + + Reservation reservation3 = reservationRepository.save( + Reservation.builder() + .reservationProducts( + List.of( + getReservationProduct( + rooms.get(1), + LocalDate.of(2023, 12, 15), + LocalDate.of(2023, 12, 18), + 360000 + ), + getReservationProduct( + rooms.get(2), + LocalDate.of(2024, 1, 10), + LocalDate.of(2024, 1, 12), + 240000 + ) + ) + ) + .member(member) + .payMethod(PayMethod.CREDIT_CARD) + .totalPrice(600000) + .build() + ); } - @Sql("classpath:testdata/reservation-service-insert.sql") @DisplayName("전체 주문 목록을 정상적으로 페이징 조회할 수 있다.") @Test void getReservationInfoList_test() { //given - long memberId = 1; + long memberId = member.getId(); PageRequest pageRequest = PageRequest.of(0, 2); //when Page result = reservationService.getReservationInfoList(memberId, pageRequest); //then - assertThat(result.getTotalElements()).isEqualTo(3); - assertThat(result.getTotalPages()).isEqualTo(2); + assertThat(result.getTotalElements()).isEqualTo(5); + assertThat(result.getTotalPages()).isEqualTo(3); log.info("{}", result.getContent().get(0)); } - @AfterEach - void tearDown() { - reservationProductRepository.deleteAll(); - reservationRepository.deleteAll(); + private ReservationProduct getReservationProduct(Room room, LocalDate startDate, LocalDate endDate, int price) { + return ReservationProduct.builder() + .room(room) + .startDate(startDate) + .endDate(endDate) + .visitorName("방문자명") + .visitorPhone("010-1111-1111") + .price(price) + .build(); + } + + private List getProductTestDataList(int size) { + List products = new ArrayList<>(); + + for (int i = 1; i <= size; i++) { + String productName = "호텔" + i; + float starAvg = ThreadLocalRandom.current().nextFloat(0, 5); + String infoCenter = String.format("02-1234-%d%d%d%d", i, i, i, i); + products.add( + productRepository.save( + Product.builder() + .name(productName) + .thumbnail(productName + " 썸네일 url") + .description(productName + " 설명") + .starAvg(starAvg) + .category(Category.TOURIST_HOTEL) + .address( + Address.builder() + .address(productName + " 주소") + .detailAddress(productName + " 상세 주소") + .mapX(1.0) + .mapY(1.5) + .build() + ) + .productOption( + ProductOption.builder() + .cooking(true) + .foodPlace("음료 가능") + .parking(true) + .pickup(false) + .infoCenter(infoCenter) + .build() + ) + .amenity( + Amenity.builder() + .barbecue(false) + .beauty(true) + .beverage(true) + .fitness(true) + .bicycle(false) + .campfire(false) + .karaoke(true) + .publicBath(true) + .publicPc(true) + .seminar(false) + .sports(false) + .build() + ) + .build() + ) + ); + } + + return products; + } + + private List getRoomTestDataList(int size, List products) { + List rooms = new ArrayList<>(); + for (int i = 1; i <= size; i++) { + String roomName = "객실" + i; + rooms.add( + roomRepository.save( + Room.builder() + .code(1000 + i) + .product(products.get((i - 1) % products.size())) + .name(roomName) + .description(roomName + " 설명") + .standard(2) + .capacity(4) + .checkIn(LocalTime.of(14, 0)) + .checkOut(LocalTime.of(12, 0)) + .price( + RoomPrice.builder() + .offWeekDaysMinFee(75000) + .offWeekendMinFee(85000) + .peakWeekDaysMinFee(100000) + .peakWeekendMinFee(120000) + .build() + ) + .roomOption( + RoomOption.builder() + .cooking(true) + .airCondition(true) + .bath(true) + .bathFacility(true) + .pc(false) + .diningTable(true) + .hairDryer(true) + .homeTheater(false) + .internet(true) + .cable(false) + .refrigerator(true) + .sofa(true) + .toiletries(true) + .tv(true) + .build() + ) + .build() + ) + ); + } + + return rooms; } } diff --git a/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/service/ReservationServiceTest.java b/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/service/ReservationServiceTest.java index 9ef9533c..30cb9cec 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/service/ReservationServiceTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/reservation/unit/service/ReservationServiceTest.java @@ -1,209 +1,373 @@ package com.fc.shimpyo_be.domain.reservation.unit.service; +import com.fc.shimpyo_be.config.AbstractContainersSupport; +import com.fc.shimpyo_be.config.DatabaseCleanUp; +import com.fc.shimpyo_be.config.TestDBCleanerConfig; +import com.fc.shimpyo_be.domain.cart.entity.Cart; +import com.fc.shimpyo_be.domain.cart.repository.CartRepository; import com.fc.shimpyo_be.domain.member.entity.Authority; import com.fc.shimpyo_be.domain.member.entity.Member; import com.fc.shimpyo_be.domain.member.exception.MemberNotFoundException; import com.fc.shimpyo_be.domain.member.repository.MemberRepository; -import com.fc.shimpyo_be.domain.product.exception.RoomNotFoundException; +import com.fc.shimpyo_be.domain.product.entity.*; +import com.fc.shimpyo_be.domain.room.exception.RoomNotFoundException; +import com.fc.shimpyo_be.domain.product.repository.ProductRepository; import com.fc.shimpyo_be.domain.reservation.dto.request.SaveReservationRequestDto; import com.fc.shimpyo_be.domain.reservation.dto.response.SaveReservationResponseDto; import com.fc.shimpyo_be.domain.reservation.entity.PayMethod; -import com.fc.shimpyo_be.domain.reservation.entity.Reservation; -import com.fc.shimpyo_be.domain.reservation.repository.ReservationRepository; import com.fc.shimpyo_be.domain.reservation.service.ReservationService; import com.fc.shimpyo_be.domain.reservationproduct.dto.request.ReservationProductRequestDto; -import com.fc.shimpyo_be.domain.reservationproduct.repository.ReservationProductRepository; import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.domain.room.entity.RoomOption; +import com.fc.shimpyo_be.domain.room.entity.RoomPrice; import com.fc.shimpyo_be.domain.room.repository.RoomRepository; +import com.fc.shimpyo_be.global.util.DateTimeUtil; +import lombok.extern.slf4j.Slf4j; +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.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.Optional; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -@ExtendWith(MockitoExtension.class) -public class ReservationServiceTest { +@Slf4j +@Import(TestDBCleanerConfig.class) +@SpringBootTest +public class ReservationServiceTest extends AbstractContainersSupport { - @InjectMocks + @Autowired private ReservationService reservationService; - @Mock - private ReservationRepository reservationRepository; - - @Mock - private ReservationProductRepository reservationProductRepository; - - @Mock + @Autowired private MemberRepository memberRepository; - @Mock + @Autowired + private ProductRepository productRepository; + + @Autowired private RoomRepository roomRepository; - @Mock - private RedisTemplate redisTemplate; + @Autowired + private CartRepository cartRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + private Member member; + + private final String[] tableNameArray = { + "member", "product", "room", "product_option", "address", "room_option", "amenity", "cart" + }; + + private LocalDate startDate1 = LocalDate.now().plusDays(1); + private LocalDate endDate1 = startDate1.plusDays(2); + + private LocalDate startDate2 = LocalDate.now().plusMonths(2); + private LocalDate endDate2 = startDate2.plusDays(3); + + @BeforeEach + void setUp() { + databaseCleanUp.cleanUp(tableNameArray); + + member = memberRepository.save( + Member.builder() + .email("member@email.com") + .name("member") + .password("password") + .photoUrl("member photo url") + .authority(Authority.ROLE_USER) + .build() + ); + + List products = getProductTestDataList(3); + List rooms = getRoomTestDataList(3, products); + + cartRepository.save( + Cart.builder() + .member(member) + .roomCode(rooms.get(0).getCode()) + .startDate(startDate1) + .endDate(endDate1) + .price(150000L) + .build() + ); - private final Member member - = Member.builder() - .id(1L) - .name("member") - .password("password") - .authority(Authority.ROLE_USER) - .photoUrl("photoUrl") - .email("email") - .build(); + cartRepository.save( + Cart.builder() + .member(member) + .roomCode(rooms.get(1).getCode()) + .startDate(startDate2) + .endDate(endDate2) + .price(200000L) + .build() + ); + } @DisplayName("정상적으로 예약을 저장할 수 있다.") @Test void saveReservation_test() { //given - long memberId = 1L; + long memberId = member.getId(); long roomId1 = 1L; long roomId2 = 2L; - Room room = Room.builder() - .id(1L) - .name("room1") - .description("description") - .price(50000) - .build(); - SaveReservationRequestDto requestDto = new SaveReservationRequestDto( List.of( - getReservationProductRequestData( - roomId1, "2023-11-20", "2023-11-23", + new ReservationProductRequestDto( + 1L, roomId1, startDate1.toString(), endDate1.toString(), "visitor1", "010-1111-1111", 150000), - getReservationProductRequestData( - roomId2, "2023-11-18", "2023-11-20", - "visitor2", "010-2222-2222", 200000) + new ReservationProductRequestDto( + 2L, roomId2, startDate2.toString(), endDate2.toString(), + "visitor1", "010-1111-1111", 200000 + ) ), PayMethod.CREDIT_CARD, 350000 ); - Reservation reservation = Reservation.builder() - .id(1L) - .totalPrice(150000) - .payMethod(PayMethod.CREDIT_CARD) - .build(); - - given(memberRepository.findById(anyLong())) - .willReturn(Optional.of(member)); - given(roomRepository.findById(anyLong())) - .willReturn(Optional.of(room)); - given(reservationRepository.save(any(Reservation.class))) - .willReturn(reservation); + Map> map = new HashMap<>(); + for (ReservationProductRequestDto reservationProduct : requestDto.reservationProducts()) { + map.put( + reservationProduct.roomId(), + getKeyList(reservationProduct.roomId(), reservationProduct.startDate(), reservationProduct.endDate()) + ); + } //when - SaveReservationResponseDto result = reservationService.saveReservation(memberId, requestDto); + SaveReservationResponseDto result = reservationService.saveReservation(memberId, requestDto, map); //then assertThat(result.reservationId()).isNotNull(); + assertThat(result.reservationProducts()).hasSize(2); + assertThat(cartRepository.findById(1L)).isNotPresent(); + assertThat(cartRepository.findById(2L)).isNotPresent(); + } + + @DisplayName("예약 저장시 장바구니 식별자가 -1인 경우는 장바구니 아이템을 삭제하지 않는다.") + @Test + void saveReservation_cart_delete_filter_test() { + //given + long memberId = member.getId(); + long roomId1 = 1L; + long cartId1 = -1L; + + SaveReservationRequestDto requestDto + = new SaveReservationRequestDto( + List.of( + new ReservationProductRequestDto( + cartId1, roomId1, startDate1.toString(), endDate1.toString(), + "visitor1", "010-1111-1111", 150000) + ), PayMethod.CREDIT_CARD, 150000 + ); - verify(memberRepository, times(1)).findById(anyLong()); - verify(roomRepository, times(2)).findById(anyLong()); - verify(reservationRepository, times(1)).save(any(Reservation.class)); + Map> map = new HashMap<>(); + for (ReservationProductRequestDto reservationProduct : requestDto.reservationProducts()) { + map.put( + reservationProduct.roomId(), + getKeyList(reservationProduct.roomId(), reservationProduct.startDate(), reservationProduct.endDate()) + ); + } + + //when + SaveReservationResponseDto result = reservationService.saveReservation(memberId, requestDto, map); + + //then + assertThat(result.reservationId()).isNotNull(); + assertThat(result.reservationProducts()).hasSize(1); + assertThat(cartRepository.findAll()).hasSize(2); } @DisplayName("회원이 존재하지 않으면 예약을 저장할 수 없다.") @Test void saveReservation_memberNotFound_test() { //given - long memberId = 1L; + long memberId = 1000L; long roomId1 = 1L; long roomId2 = 2L; SaveReservationRequestDto requestDto = new SaveReservationRequestDto( List.of( - getReservationProductRequestData( - roomId1, "2023-11-20", "2023-11-23", + new ReservationProductRequestDto( + 1L, roomId1, startDate1.toString(), endDate1.toString(), "visitor1", "010-1111-1111", 150000), - getReservationProductRequestData( - roomId2, "2023-11-18", "2023-11-20", - "visitor2", "010-2222-2222", 200000) + new ReservationProductRequestDto( + 2L, roomId2, startDate2.toString(), endDate2.toString(), + "visitor1", "010-1111-1111", 200000 + ) ), PayMethod.CREDIT_CARD, 350000 ); - willThrow(MemberNotFoundException.class) - .given(memberRepository).findById(anyLong()); + Map> map = new HashMap<>(); + for (ReservationProductRequestDto reservationProduct : requestDto.reservationProducts()) { + map.put( + reservationProduct.roomId(), + getKeyList(reservationProduct.roomId(), reservationProduct.startDate(), reservationProduct.endDate()) + ); + } //when & then - assertThatThrownBy(() -> reservationService.saveReservation(memberId, requestDto)) + assertThatThrownBy(() -> reservationService.saveReservation(memberId, requestDto, map)) .isInstanceOf(MemberNotFoundException.class); - - verify(memberRepository, times(1)).findById(anyLong()); - verify(roomRepository, times(0)).findById(anyLong()); - verify(reservationRepository, times(0)).save(any(Reservation.class)); } @DisplayName("객실 정보가 존재하지 않으면 예약을 저장할 수 없다.") @Test void saveReservation_roomNotFound_test() { //given - long memberId = 1L; + long memberId = member.getId(); long roomId1 = 1L; - long roomId2 = 2L; + long roomId2 = 2000L; SaveReservationRequestDto requestDto = new SaveReservationRequestDto( List.of( - getReservationProductRequestData( - roomId1, "2023-11-20", "2023-11-23", + new ReservationProductRequestDto( + 1L, roomId1, startDate1.toString(), endDate1.toString(), "visitor1", "010-1111-1111", 150000), - getReservationProductRequestData( - roomId2, "2023-11-18", "2023-11-20", - "visitor2", "010-2222-2222", 200000) + new ReservationProductRequestDto( + 2L, roomId2, startDate2.toString(), endDate2.toString(), + "visitor1", "010-1111-1111", 200000 + ) ), PayMethod.CREDIT_CARD, 350000 ); - given(memberRepository.findById(anyLong())) - .willReturn(Optional.of(member)); - willThrow(RoomNotFoundException.class) - .given(roomRepository).findById(anyLong()); + Map> map = new HashMap<>(); + for (ReservationProductRequestDto reservationProduct : requestDto.reservationProducts()) { + map.put( + reservationProduct.roomId(), + getKeyList(reservationProduct.roomId(), reservationProduct.startDate(), reservationProduct.endDate()) + ); + } //when & then - assertThatThrownBy(() -> reservationService.saveReservation(memberId, requestDto)) + assertThatThrownBy(() -> reservationService.saveReservation(memberId, requestDto, map)) .isInstanceOf(RoomNotFoundException.class); + } - verify(memberRepository, times(1)).findById(anyLong()); - verify(roomRepository, times(1)).findById(anyLong()); - verify(reservationRepository, times(0)).save(any(Reservation.class)); + private List getKeyList(Long roomId, String startDate, String endDate) { + List keyList = new ArrayList<>(); + + LocalDate targetDate = DateTimeUtil.toLocalDate(startDate); + LocalDate maxDate = DateTimeUtil.toLocalDate(endDate); + while (targetDate.isBefore(maxDate)) { + keyList.add("roomId:" + roomId + ":" + targetDate); + targetDate = targetDate.plusDays(1); + } + + return keyList; } - private ReservationProductRequestDto getReservationProductRequestData( - long roomId, - String startDate, - String endDate, - String visitorName, - String visitorPhone, - Integer price - ) { - String defaultValue = "DEFAULT_VALUE"; - return new ReservationProductRequestDto( - roomId, - defaultValue, - defaultValue, - 2, - 4, - startDate, - endDate, - defaultValue, - defaultValue, - visitorName, - visitorPhone, - price - ); + private List getProductTestDataList(int size) { + List products = new ArrayList<>(); + + for (int i = 1; i <= size; i++) { + String productName = "호텔" + i; + float starAvg = ThreadLocalRandom.current().nextFloat(0, 5); + String infoCenter = String.format("02-1234-%d%d%d%d", i, i, i, i); + products.add( + productRepository.save( + Product.builder() + .name(productName) + .thumbnail(productName + " 썸네일 url") + .description(productName + " 설명") + .starAvg(starAvg) + .category(Category.TOURIST_HOTEL) + .address( + Address.builder() + .address(productName + " 주소") + .detailAddress(productName + " 상세 주소") + .mapX(1.0) + .mapY(1.5) + .build() + ) + .productOption( + ProductOption.builder() + .cooking(true) + .foodPlace("음료 가능") + .parking(true) + .pickup(false) + .infoCenter(infoCenter) + .build() + ) + .amenity( + Amenity.builder() + .barbecue(false) + .beauty(true) + .beverage(true) + .fitness(true) + .bicycle(false) + .campfire(false) + .karaoke(true) + .publicBath(true) + .publicPc(true) + .seminar(false) + .sports(false) + .build() + ) + .build() + ) + ); + } + + return products; + } + + private List getRoomTestDataList(int size, List products) { + List rooms = new ArrayList<>(); + for (int i = 1; i <= size; i++) { + String roomName = "객실" + i; + rooms.add( + roomRepository.save( + Room.builder() + .code(1000 + i) + .product(products.get((i - 1) % products.size())) + .name(roomName) + .description(roomName + " 설명") + .standard(2) + .capacity(4) + .checkIn(LocalTime.of(14, 0)) + .checkOut(LocalTime.of(12, 0)) + .price( + RoomPrice.builder() + .offWeekDaysMinFee(75000) + .offWeekendMinFee(85000) + .peakWeekDaysMinFee(100000) + .peakWeekendMinFee(120000) + .build() + ) + .roomOption( + RoomOption.builder() + .cooking(true) + .airCondition(true) + .bath(true) + .bathFacility(true) + .pc(false) + .diningTable(true) + .hairDryer(true) + .homeTheater(false) + .internet(true) + .cable(false) + .refrigerator(true) + .sofa(true) + .toiletries(true) + .tv(true) + .build() + ) + .build() + ) + ); + } + + return rooms; } } diff --git a/src/test/java/com/fc/shimpyo_be/domain/reservationproduct/docs/ReservationProductRestControllerDocsTest.java b/src/test/java/com/fc/shimpyo_be/domain/reservationproduct/docs/ReservationProductRestControllerDocsTest.java index 7a0a2731..8be980c5 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/reservationproduct/docs/ReservationProductRestControllerDocsTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/reservationproduct/docs/ReservationProductRestControllerDocsTest.java @@ -25,7 +25,7 @@ public class ReservationProductRestControllerDocsTest extends RestDocsSupport { private SecurityUtil securityUtil; @WithMockUser(roles = "USER") - @DisplayName("cancel()는 예약 주문 상품을 취소할 수 있다.") + @DisplayName("cancel()는 예약 주문 숙소을 취소할 수 있다.") @Test void cancel() throws Exception { // given diff --git a/src/test/java/com/fc/shimpyo_be/domain/reservationproduct/unit/controller/ReservationProductRestControllerTest.java b/src/test/java/com/fc/shimpyo_be/domain/reservationproduct/unit/controller/ReservationProductRestControllerTest.java index bdaf7955..62636dca 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/reservationproduct/unit/controller/ReservationProductRestControllerTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/reservationproduct/unit/controller/ReservationProductRestControllerTest.java @@ -45,7 +45,7 @@ void setUp(@Autowired WebApplicationContext applicationContext) { } @WithMockUser(roles = "USER") - @DisplayName("[api][DELETE][정상] 예약 주문 상품 취소 API 테스트") + @DisplayName("[api][DELETE][정상] 예약 주문 숙소 취소 API 테스트") @Test void saveReservation_Api_test() throws Exception { //given diff --git a/src/test/java/com/fc/shimpyo_be/domain/reservationproduct/unit/service/ReservationProductServiceTest.java b/src/test/java/com/fc/shimpyo_be/domain/reservationproduct/unit/service/ReservationProductServiceTest.java index 04b86452..08893985 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/reservationproduct/unit/service/ReservationProductServiceTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/reservationproduct/unit/service/ReservationProductServiceTest.java @@ -36,7 +36,7 @@ class ReservationProductServiceTest { @Mock private ReservationProductRepository reservationProductRepository; - @DisplayName("예약 상품을 취소할 수 있다.") + @DisplayName("예약 숙소을 취소할 수 있다.") @Test void cancel_test() { //given @@ -79,7 +79,7 @@ void cancel_test() { verify(reservationProductRepository, times(1)).findByIdWithReservation(anyLong()); } - @DisplayName("예약 상품 정보가 존재하지 않으면, 예약 상품을 취소할 수 있다.") + @DisplayName("예약 숙소 정보가 존재하지 않으면, 예약 숙소을 취소할 수 있다.") @Test void cancel_reservationProductNotFound_test() { //given @@ -96,7 +96,7 @@ void cancel_reservationProductNotFound_test() { verify(reservationProductRepository, times(1)).findByIdWithReservation(anyLong()); } - @DisplayName("예약 상품 주문자 정보와 현재 인증 객체 정보가 일치하지 않으며, 예약 상품을 취소할 수 없다.") + @DisplayName("예약 숙소 주문자 정보와 현재 인증 객체 정보가 일치하지 않으며, 예약 숙소을 취소할 수 없다.") @Test void cancel_forbidden_test() { //given diff --git a/src/test/java/com/fc/shimpyo_be/domain/room/docs/RoomRestControllerDocsTest.java b/src/test/java/com/fc/shimpyo_be/domain/room/docs/RoomRestControllerDocsTest.java new file mode 100644 index 00000000..74ac6c7a --- /dev/null +++ b/src/test/java/com/fc/shimpyo_be/domain/room/docs/RoomRestControllerDocsTest.java @@ -0,0 +1,126 @@ +package com.fc.shimpyo_be.domain.room.docs; + +import com.fc.shimpyo_be.config.RestDocsSupport; +import com.fc.shimpyo_be.domain.room.dto.response.RoomWithProductResponseDto; +import com.fc.shimpyo_be.domain.room.service.RoomService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.security.test.context.support.WithMockUser; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class RoomRestControllerDocsTest extends RestDocsSupport { + + @MockBean + private RoomService roomService; + + @WithMockUser(roles = "USER") + @DisplayName("getRoomsWithProductInfo()는 숙소 정보를 포함한 객실 정보 리스트를 조회할 수 있다.") + @Test + void getRoomsWithProductInfo() throws Exception { + //given + String requestUrl = "/api/rooms"; + String roomIdParamList = "1,3,4"; + List roomIds = List.of(1L, 3L, 4L); + + List rooms = List.of( + RoomWithProductResponseDto.builder() + .productId(1L) + .productName("호텔1") + .productThumbnail("호텔1 썸네일") + .productAddress("호텔1 주소") + .productDetailAddress("호텔1 상세 주소") + .roomId(1L) + .roomName("객실1") + .standard(2) + .capacity(4) + .checkIn("14:00") + .checkOut("12:00") + .price(80000L) + .build(), + RoomWithProductResponseDto.builder() + .productId(2L) + .productName("호텔2") + .productThumbnail("호텔2 썸네일") + .productAddress("호텔2 주소") + .productDetailAddress("호텔2 상세 주소") + .roomId(3L) + .roomName("객실3") + .standard(2) + .capacity(4) + .checkIn("14:00") + .checkOut("11:30") + .price(95000L) + .build(), + RoomWithProductResponseDto.builder() + .productId(3L) + .productName("호텔3") + .productThumbnail("호텔3 썸네일") + .productAddress("호텔3 주소") + .productDetailAddress("호텔3 상세 주소") + .roomId(4L) + .roomName("객실4") + .standard(2) + .capacity(4) + .checkIn("13:00") + .checkOut("11:00") + .price(80000L) + .build() + ); + + given(roomService.getRoomsWithProductInfo(roomIds)) + .willReturn(rooms); + + //when & then + mockMvc.perform( + get(requestUrl) + .queryParam("roomIds", roomIdParamList) + .characterEncoding(StandardCharsets.UTF_8) + ) + .andExpect(status().isOk()) + .andDo(restDoc.document( + queryParameters( + parameterWithName("roomIds").description("숙소/객실 정보를 조회할 객실 식별자 리스트") + .attributes(key("constraints").value( + List.of( + "최소 1개, 최대 3개의 객실 식별자 정보가 필요합니다.", + "객실 식별자는 최소 1 이상이어야 합니다." + ) + )) + ), + responseFields(responseCommon()).and( + fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), + fieldWithPath("data.rooms").type(JsonFieldType.ARRAY).description("조회한 객실 정보 리스트"), + fieldWithPath("data.rooms[].productId").type(JsonFieldType.NUMBER).description("숙소 식별자"), + fieldWithPath("data.rooms[].productName").type(JsonFieldType.STRING).description("숙소명"), + fieldWithPath("data.rooms[].productThumbnail").type(JsonFieldType.STRING).description("숙소 썸네일 이미지 URL"), + fieldWithPath("data.rooms[].productAddress").type(JsonFieldType.STRING).description("숙소 주소"), + fieldWithPath("data.rooms[].productDetailAddress").type(JsonFieldType.STRING).description("숙소 상세 주소"), + fieldWithPath("data.rooms[].roomId").type(JsonFieldType.NUMBER).description("객실 식별자"), + fieldWithPath("data.rooms[].roomName").type(JsonFieldType.STRING).description("객실명"), + fieldWithPath("data.rooms[].standard").type(JsonFieldType.NUMBER).description("기준 인원"), + fieldWithPath("data.rooms[].capacity").type(JsonFieldType.NUMBER).description("최대 인원"), + fieldWithPath("data.rooms[].checkIn").type(JsonFieldType.STRING).description("체크인 시간"), + fieldWithPath("data.rooms[].checkOut").type(JsonFieldType.STRING).description("체크아웃 시간"), + fieldWithPath("data.rooms[].price").type(JsonFieldType.NUMBER).description("객실 가격") + ) + ) + ); + + verify(roomService, times(1)).getRoomsWithProductInfo(roomIds); + } +} diff --git a/src/test/java/com/fc/shimpyo_be/domain/room/unit/controller/RoomRestControllerTest.java b/src/test/java/com/fc/shimpyo_be/domain/room/unit/controller/RoomRestControllerTest.java new file mode 100644 index 00000000..8649321a --- /dev/null +++ b/src/test/java/com/fc/shimpyo_be/domain/room/unit/controller/RoomRestControllerTest.java @@ -0,0 +1,230 @@ +package com.fc.shimpyo_be.domain.room.unit.controller; + +import com.fc.shimpyo_be.config.AbstractContainersSupport; +import com.fc.shimpyo_be.domain.room.dto.response.RoomWithProductResponseDto; +import com.fc.shimpyo_be.domain.room.service.RoomService; +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.boot.test.mock.mockito.MockBean; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.hamcrest.Matchers.*; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +public class RoomRestControllerTest extends AbstractContainersSupport { + + private MockMvc mockMvc; + + @MockBean + private RoomService roomService; + + @BeforeEach + void setUp(@Autowired WebApplicationContext applicationContext) { + this.mockMvc = MockMvcBuilders + .webAppContextSetup(applicationContext) + .apply(springSecurity()) + .alwaysDo(print()) + .build(); + } + + @WithMockUser(roles = "USER") + @DisplayName("[api][GET][정상] 숙소 정보 포함 객실 정보 리스트 조회 API 테스트 - 콤마 리스트 파라메터") + @Test + void getRoomsWithProductInfo_api_test() throws Exception { + //given + String requestUrl = "/api/rooms"; + String roomIdParamList = "1, 2, 3"; + List roomIds = List.of(1L, 2L, 3L); + + List rooms = List.of( + RoomWithProductResponseDto.builder() + .productId(1L) + .productName("호텔1") + .productThumbnail("호텔1 썸네일") + .productAddress("호텔1 주소") + .productDetailAddress("호텔1 상세 주소") + .roomId(1L) + .roomName("객실1") + .standard(2) + .capacity(4) + .checkIn("14:00") + .checkOut("12:00") + .price(80000L) + .build(), + RoomWithProductResponseDto.builder() + .productId(2L) + .productName("호텔2") + .productThumbnail("호텔2 썸네일") + .productAddress("호텔2 주소") + .productDetailAddress("호텔2 상세 주소") + .roomId(3L) + .roomName("객실3") + .standard(2) + .capacity(4) + .checkIn("14:00") + .checkOut("11:30") + .price(95000L) + .build(), + RoomWithProductResponseDto.builder() + .productId(3L) + .productName("호텔3") + .productThumbnail("호텔3 썸네일") + .productAddress("호텔3 주소") + .productDetailAddress("호텔3 상세 주소") + .roomId(4L) + .roomName("객실4") + .standard(2) + .capacity(4) + .checkIn("13:00") + .checkOut("11:00") + .price(95000L) + .build() + ); + + given(roomService.getRoomsWithProductInfo(roomIds)) + .willReturn(rooms); + + //when & then + mockMvc.perform( + get(requestUrl) + .param("roomIds", roomIdParamList) + .characterEncoding(StandardCharsets.UTF_8) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code", is(200))) + .andExpect(jsonPath("$.data").isNotEmpty()); + + verify(roomService, times(1)).getRoomsWithProductInfo(roomIds); + } + + @WithMockUser(roles = "USER") + @DisplayName("[api][GET][정상] 숙소 정보 포함 객실 정보 리스트 조회 API 테스트 - 파라메터 개별로 매핑") + @Test + void getRoomsWithProductInfo_api_request_params_test() throws Exception { + //given + String requestUrl = "/api/rooms"; + List roomIds = List.of(1L, 2L, 3L); + + List rooms = List.of( + RoomWithProductResponseDto.builder() + .productId(1L) + .productName("호텔1") + .productThumbnail("호텔1 썸네일") + .productAddress("호텔1 주소") + .productDetailAddress("호텔1 상세 주소") + .roomId(1L) + .roomName("객실1") + .standard(2) + .capacity(4) + .checkIn("14:00") + .checkOut("12:00") + .price(80000L) + .build(), + RoomWithProductResponseDto.builder() + .productId(2L) + .productName("호텔2") + .productThumbnail("호텔2 썸네일") + .productAddress("호텔2 주소") + .productDetailAddress("호텔2 상세 주소") + .roomId(3L) + .roomName("객실3") + .standard(2) + .capacity(4) + .checkIn("14:00") + .checkOut("11:30") + .price(95000L) + .build(), + RoomWithProductResponseDto.builder() + .productId(3L) + .productName("호텔3") + .productThumbnail("호텔3 썸네일") + .productAddress("호텔3 주소") + .productDetailAddress("호텔3 상세 주소") + .roomId(4L) + .roomName("객실4") + .standard(2) + .capacity(4) + .checkIn("13:00") + .checkOut("11:00") + .price(95000L) + .build() + ); + + given(roomService.getRoomsWithProductInfo(roomIds)) + .willReturn(rooms); + + //when & then + mockMvc.perform( + get(requestUrl) + .param("roomIds", String.valueOf(roomIds.get(0))) + .param("roomIds", String.valueOf(roomIds.get(1))) + .param("roomIds", String.valueOf(roomIds.get(2))) + .characterEncoding(StandardCharsets.UTF_8) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code", is(200))) + .andExpect(jsonPath("$.data").isNotEmpty()); + + verify(roomService, times(1)).getRoomsWithProductInfo(roomIds); + } + + @WithMockUser(roles = "USER") + @DisplayName("[api][GET][실패] 숙소 정보 포함 객실 정보 리스트 조회 API 테스트 - 요청 데이터 검증 에러(Size)") + @Test + void getRoomsWithProductInfo_api_size_validation_fail_test() throws Exception { + //given + String requestUrl = "/api/rooms"; + String roomIdParamList = "1, 3, 4, 5"; + List roomIds = List.of(1L, 3L, 4L, 5L); + + + //when & then + mockMvc.perform( + get(requestUrl) + .param("roomIds", roomIdParamList) + .characterEncoding(StandardCharsets.UTF_8) + ) + .andExpect(status().is4xxClientError()) + .andExpect(jsonPath("$.code", is(400))); + + verify(roomService, times(0)).getRoomsWithProductInfo(roomIds); + } + + @WithMockUser(roles = "USER") + @DisplayName("[api][GET][실패] 숙소 정보 포함 객실 정보 리스트 조회 API 테스트 - 요청 데이터 검증 에러(Min)") + @Test + void getRoomsWithProductInfo_api_min_validation_fail_test() throws Exception { + //given + String requestUrl = "/api/rooms"; + String roomIdParamList = "-2, 3"; + + //when & then + mockMvc.perform( + get(requestUrl) + .param("roomIds", roomIdParamList) + .characterEncoding(StandardCharsets.UTF_8) + ) + .andExpect(status().is4xxClientError()) + .andExpect(jsonPath("$.code", is(400))); + + verify(roomService, times(0)).getRoomsWithProductInfo(anyList()); + } +} diff --git a/src/test/java/com/fc/shimpyo_be/domain/room/unit/repository/RoomRepositoryTest.java b/src/test/java/com/fc/shimpyo_be/domain/room/unit/repository/RoomRepositoryTest.java new file mode 100644 index 00000000..7956401c --- /dev/null +++ b/src/test/java/com/fc/shimpyo_be/domain/room/unit/repository/RoomRepositoryTest.java @@ -0,0 +1,153 @@ +package com.fc.shimpyo_be.domain.room.unit.repository; + +import com.fc.shimpyo_be.config.DatabaseCleanUp; +import com.fc.shimpyo_be.config.TestDBCleanerConfig; +import com.fc.shimpyo_be.config.TestQuerydslConfig; +import com.fc.shimpyo_be.domain.product.entity.*; +import com.fc.shimpyo_be.domain.product.repository.ProductRepository; +import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.domain.room.entity.RoomOption; +import com.fc.shimpyo_be.domain.room.entity.RoomPrice; +import com.fc.shimpyo_be.domain.room.repository.RoomRepository; +import lombok.extern.slf4j.Slf4j; +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.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.time.LocalTime; +import java.util.LinkedList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +@Import({TestQuerydslConfig.class, TestDBCleanerConfig.class}) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@DataJpaTest +public class RoomRepositoryTest { + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + private final String[] tableNameArray = { + "product", "room", "product_option", "address", "room_option", "amenity", "room_price" + }; + + private List roomIds; + + @BeforeEach + void setUp() { + databaseCleanUp.cleanUp(tableNameArray); + + Product product = productRepository.save( + Product.builder() + .name("호텔") + .thumbnail("호텔 썸네일 url") + .description("호텔 설명") + .starAvg(4.2f) + .category(Category.TOURIST_HOTEL) + .address( + Address.builder() + .address("호텔 주소") + .detailAddress("호텔 상세 주소") + .mapX(1.0) + .mapY(1.5) + .build() + ) + .productOption( + ProductOption.builder() + .cooking(true) + .foodPlace("음료 가능") + .parking(true) + .pickup(false) + .infoCenter("1500-0000") + .build() + ) + .amenity( + Amenity.builder() + .barbecue(false) + .beauty(true) + .beverage(true) + .fitness(true) + .bicycle(false) + .campfire(false) + .karaoke(true) + .publicBath(true) + .publicPc(true) + .seminar(false) + .sports(false) + .build() + ) + .build() + ); + + roomIds = new LinkedList<>(); + + for (int i = 1; i <= 5; i++) { + String roomName = "호텔 객실" + i; + roomIds.add( + roomRepository.save( + Room.builder() + .product(product) + .name(roomName) + .description(roomName + " 설명") + .standard(2) + .capacity(4) + .checkIn(LocalTime.of(14, 0)) + .checkOut(LocalTime.of(12, 0)) + .price( + RoomPrice.builder() + .offWeekDaysMinFee(75000) + .offWeekendMinFee(85000) + .peakWeekDaysMinFee(100000) + .peakWeekendMinFee(120000) + .build() + ) + .roomOption( + RoomOption.builder() + .cooking(true) + .airCondition(true) + .bath(true) + .bathFacility(true) + .pc(false) + .diningTable(true) + .hairDryer(true) + .homeTheater(false) + .internet(true) + .cable(false) + .refrigerator(true) + .sofa(true) + .toiletries(true) + .tv(true) + .build() + ) + .build() + ).getId() + ); + } + } + + @DisplayName("findAllInIdsWithProductAndPrice 테스트") + @Test + void findAllInIdsWithProductAndPrice() { + //given + + //when + List result = roomRepository.findAllInIdsWithProductAndPrice(roomIds); + + //then + assertThat(result).hasSize(5); + assertThat(result.get(0).getProduct().getId()).isEqualTo(1); + assertThat(result.get(0).getPrice()).isNotNull(); + } +} diff --git a/src/test/java/com/fc/shimpyo_be/domain/room/unit/service/RoomServiceTest.java b/src/test/java/com/fc/shimpyo_be/domain/room/unit/service/RoomServiceTest.java new file mode 100644 index 00000000..0bd36745 --- /dev/null +++ b/src/test/java/com/fc/shimpyo_be/domain/room/unit/service/RoomServiceTest.java @@ -0,0 +1,134 @@ +package com.fc.shimpyo_be.domain.room.unit.service; + +import com.fc.shimpyo_be.domain.product.entity.*; +import com.fc.shimpyo_be.domain.room.dto.response.RoomWithProductResponseDto; +import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.domain.room.entity.RoomOption; +import com.fc.shimpyo_be.domain.room.entity.RoomPrice; +import com.fc.shimpyo_be.domain.room.repository.RoomRepository; +import com.fc.shimpyo_be.domain.room.service.RoomService; +import org.junit.jupiter.api.DisplayName; +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; + +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class RoomServiceTest { + + @InjectMocks + private RoomService roomService; + + @Mock + private RoomRepository roomRepository; + + @DisplayName("객실 식별자 리스트에 해당하는 객실과 숙소 정보를 리스트로 반환한다.") + @Test + void getRoomsWithProductInfo_test() { + //given + List roomIds = List.of(1L, 3L, 4L); + + Product product = Product.builder() + .name("호텔") + .thumbnail("호텔 썸네일 url") + .description("호텔 설명") + .starAvg(4.2f) + .category(Category.TOURIST_HOTEL) + .address( + Address.builder() + .address("호텔 주소") + .detailAddress("호텔 상세 주소") + .mapX(1.0) + .mapY(1.5) + .build() + ) + .productOption( + ProductOption.builder() + .cooking(true) + .foodPlace("음료 가능") + .parking(true) + .pickup(false) + .infoCenter("1500-0000") + .build() + ) + .amenity( + Amenity.builder() + .barbecue(false) + .beauty(true) + .beverage(true) + .fitness(true) + .bicycle(false) + .campfire(false) + .karaoke(true) + .publicBath(true) + .publicPc(true) + .seminar(false) + .sports(false) + .build() + ) + .build(); + + List rooms = new ArrayList<>(); + + for (int i = 1; i <= 3; i++) { + String roomName = "호텔 객실" + i; + rooms.add( + Room.builder() + .product(product) + .name(roomName) + .description(roomName + " 설명") + .standard(2) + .capacity(4) + .checkIn(LocalTime.of(14, 0)) + .checkOut(LocalTime.of(12, 0)) + .price( + RoomPrice.builder() + .offWeekDaysMinFee(75000) + .offWeekendMinFee(85000) + .peakWeekDaysMinFee(100000) + .peakWeekendMinFee(120000) + .build() + ) + .roomOption( + RoomOption.builder() + .cooking(true) + .airCondition(true) + .bath(true) + .bathFacility(true) + .pc(false) + .diningTable(true) + .hairDryer(true) + .homeTheater(false) + .internet(true) + .cable(false) + .refrigerator(true) + .sofa(true) + .toiletries(true) + .tv(true) + .build() + ) + .build() + ); + } + + given(roomRepository.findAllInIdsWithProductAndPrice(roomIds)).willReturn(rooms); + + //when + List result = roomService.getRoomsWithProductInfo(roomIds); + + //then + assertThat(result).hasSize(3); + + verify(roomRepository, times(1)).findAllInIdsWithProductAndPrice(roomIds); + } +} diff --git a/src/test/java/com/fc/shimpyo_be/domain/star/docs/StarRestControllerDocsTest.java b/src/test/java/com/fc/shimpyo_be/domain/star/docs/StarRestControllerDocsTest.java index 29179519..eac8188b 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/star/docs/StarRestControllerDocsTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/star/docs/StarRestControllerDocsTest.java @@ -60,7 +60,7 @@ void register() throws Exception { requestFields( fieldWithPath("reservationProductId").type(JsonFieldType.NUMBER).description("별점 등록 대상 예약 상품 식별자") .attributes(key("constraints").value( - starRegisterDescriptions.descriptionsForProperty("productId"))), + starRegisterDescriptions.descriptionsForProperty("reservationProductId"))), fieldWithPath("productId").type(JsonFieldType.NUMBER).description("별점 등록 대상 숙소 식별자") .attributes(key("constraints").value( starRegisterDescriptions.descriptionsForProperty("productId"))), diff --git a/src/test/java/com/fc/shimpyo_be/domain/star/unit/controller/StarRestControllerTest.java b/src/test/java/com/fc/shimpyo_be/domain/star/unit/controller/StarRestControllerTest.java index 66050359..8484dd03 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/star/unit/controller/StarRestControllerTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/star/unit/controller/StarRestControllerTest.java @@ -4,6 +4,8 @@ import com.fc.shimpyo_be.config.AbstractContainersSupport; import com.fc.shimpyo_be.domain.star.dto.request.StarRegisterRequestDto; import com.fc.shimpyo_be.domain.star.dto.response.StarResponseDto; +import com.fc.shimpyo_be.domain.star.exception.CannotBeforeCheckOutException; +import com.fc.shimpyo_be.domain.star.exception.ExpiredRegisterDateException; import com.fc.shimpyo_be.domain.star.service.StarService; import com.fc.shimpyo_be.global.util.SecurityUtil; import org.junit.jupiter.api.BeforeEach; @@ -18,6 +20,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; @@ -50,7 +53,7 @@ void setUp(@Autowired WebApplicationContext applicationContext) { } @WithMockUser(roles = "USER") - @DisplayName("[api][POST] 별점 등록 API 성공 테스트") + @DisplayName("[api][POST][정상] 별점 등록 API 테스트") @Test void register_success_test() throws Exception { // given @@ -77,4 +80,58 @@ void register_success_test() throws Exception { .andExpect(jsonPath("$.data.starId").isNumber()) .andExpect(jsonPath("$.data.score").isNotEmpty()); } + + @WithMockUser(roles = "USER") + @DisplayName("[api][POST][에러] 별점 등록 API 테스트 - 체크아웃 이전 등록 기간 검증 에러") + @Test + void register_cannotBeforeCheckOutException_test() throws Exception { + // given + String requestUrl = "/api/stars"; + Long productId = 1L; + float score = 3.5F; + + StarRegisterRequestDto requestDto + = new StarRegisterRequestDto(1L, productId, score); + + given(securityUtil.getCurrentMemberId()).willReturn(1L); + given(starService.register(anyLong(), any(StarRegisterRequestDto.class))) + .willThrow(new CannotBeforeCheckOutException()); + + // when & then + mockMvc.perform( + post(requestUrl) + .content(objectMapper.writeValueAsString(requestDto)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code", is(400))) + .andExpect(jsonPath("$.data").isEmpty()); + } + + @WithMockUser(roles = "USER") + @DisplayName("[api][POST][에러] 별점 등록 API 테스트 - 만료된 등록 기간 검증 에러") + @Test + void register_expiredRegisterDateException_test() throws Exception { + // given + String requestUrl = "/api/stars"; + Long productId = 1L; + float score = 3.5F; + + StarRegisterRequestDto requestDto + = new StarRegisterRequestDto(1L, productId, score); + + given(securityUtil.getCurrentMemberId()).willReturn(1L); + given(starService.register(anyLong(), any(StarRegisterRequestDto.class))) + .willThrow(new ExpiredRegisterDateException()); + + // when & then + mockMvc.perform( + post(requestUrl) + .content(objectMapper.writeValueAsString(requestDto)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code", is(400))) + .andExpect(jsonPath("$.data").isEmpty()); + } } diff --git a/src/test/java/com/fc/shimpyo_be/domain/star/unit/service/StarServiceTest.java b/src/test/java/com/fc/shimpyo_be/domain/star/unit/service/StarServiceTest.java index b9b9bb82..f49f9aba 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/star/unit/service/StarServiceTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/star/unit/service/StarServiceTest.java @@ -3,7 +3,8 @@ import com.fc.shimpyo_be.domain.member.entity.Authority; import com.fc.shimpyo_be.domain.member.entity.Member; import com.fc.shimpyo_be.domain.member.exception.MemberNotFoundException; -import com.fc.shimpyo_be.domain.member.repository.MemberRepository; +import com.fc.shimpyo_be.domain.member.service.MemberService; +import com.fc.shimpyo_be.domain.product.entity.Address; import com.fc.shimpyo_be.domain.product.entity.Category; import com.fc.shimpyo_be.domain.product.entity.Product; import com.fc.shimpyo_be.domain.product.exception.ProductNotFoundException; @@ -13,10 +14,12 @@ import com.fc.shimpyo_be.domain.reservationproduct.entity.ReservationProduct; import com.fc.shimpyo_be.domain.reservationproduct.repository.ReservationProductRepository; import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.domain.room.entity.RoomPrice; import com.fc.shimpyo_be.domain.star.dto.request.StarRegisterRequestDto; import com.fc.shimpyo_be.domain.star.dto.response.StarResponseDto; import com.fc.shimpyo_be.domain.star.entity.Star; import com.fc.shimpyo_be.domain.star.exception.CannotBeforeCheckOutException; +import com.fc.shimpyo_be.domain.star.exception.ExpiredRegisterDateException; import com.fc.shimpyo_be.domain.star.repository.StarRepository; import com.fc.shimpyo_be.domain.star.service.StarService; import org.junit.jupiter.api.BeforeEach; @@ -31,7 +34,8 @@ import java.time.LocalTime; import java.util.Optional; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; @@ -49,7 +53,7 @@ public class StarServiceTest { private StarRepository starRepository; @Mock - private MemberRepository memberRepository; + private MemberService memberService; @Mock private ProductRepository productRepository; @@ -63,7 +67,9 @@ public class StarServiceTest { private ReservationProduct reservationProduct; - private ReservationProduct reservationProduct2; + private ReservationProduct reservationProductBeforeCheckOut; + + private ReservationProduct reservationProductExpiredToRegister; @BeforeEach void setUp() { @@ -83,7 +89,12 @@ void setUp() { .category(Category.TOURIST_HOTEL) .thumbnail("thumbnail url") .starAvg(3.5f) - .address("숙소 주소") + .address(Address.builder() + .address("숙소 주소") + .detailAddress("숙소 상세 주소") + .mapX(1.0) + .mapY(1.0) + .build()) .build(); Reservation reservation = Reservation.builder() @@ -96,26 +107,54 @@ void setUp() { .id(2L) .reservation(reservation) .room(Room.builder() - .price(50000) + .price(RoomPrice.builder() + .offWeekDaysMinFee(50000) + .offWeekendMinFee(60000) + .peakWeekDaysMinFee(100000) + .peakWeekendMinFee(110000) + .build()) .description("객실정보") .product(product) .checkOut(LocalTime.of(12, 0)) .build() ) - .endDate(LocalDate.of(2023, 11, 29)) + .endDate(LocalDate.now().minusDays(7)) .build(); - reservationProduct2 = ReservationProduct.builder() + reservationProductBeforeCheckOut = ReservationProduct.builder() .id(3L) .reservation(reservation) .room(Room.builder() - .price(50000) + .price(RoomPrice.builder() + .offWeekDaysMinFee(50000) + .offWeekendMinFee(60000) + .peakWeekDaysMinFee(100000) + .peakWeekendMinFee(110000) + .build()) .description("객실정보") .product(product) .checkOut(LocalTime.of(12, 0)) .build() ) - .endDate(LocalDate.of(2100, 11, 29)) + .endDate(LocalDate.now().plusDays(5)) + .build(); + + reservationProductExpiredToRegister = ReservationProduct.builder() + .id(4L) + .reservation(reservation) + .room(Room.builder() + .price(RoomPrice.builder() + .offWeekDaysMinFee(50000) + .offWeekendMinFee(60000) + .peakWeekDaysMinFee(100000) + .peakWeekendMinFee(110000) + .build()) + .description("객실정보") + .product(product) + .checkOut(LocalTime.of(12, 0)) + .build() + ) + .endDate(LocalDate.now().minusDays(15)) .build(); } @@ -127,8 +166,8 @@ void register_test() { StarRegisterRequestDto request = new StarRegisterRequestDto(reservationProduct.getId(), product.getId(), score); - given(memberRepository.findById(anyLong())) - .willReturn(Optional.of(member)); + given(memberService.getMemberById(anyLong())) + .willReturn(member); given(productRepository.findById(anyLong())) .willReturn(Optional.of(product)); given(reservationProductRepository.findByIdWithRoom(anyLong())) @@ -150,7 +189,7 @@ void register_test() { assertThat(response).isNotNull(); assertThat(response.score()).isEqualTo(score); - verify(memberRepository, times(1)).findById(anyLong()); + verify(memberService, times(1)).getMemberById(anyLong()); verify(productRepository, times(1)).findById(anyLong()); verify(reservationProductRepository, times(1)).findByIdWithRoom(anyLong()); verify(starRepository, times(1)).save(any(Star.class)); @@ -165,14 +204,14 @@ void register_memberNotFoundException() { StarRegisterRequestDto request = new StarRegisterRequestDto(1L, product.getId(), score); - given(memberRepository.findById(anyLong())) + given(memberService.getMemberById(anyLong())) .willThrow(MemberNotFoundException.class); // when & then assertThatThrownBy(() -> starService.register(memberId, request)) .isInstanceOf(MemberNotFoundException.class); - verify(memberRepository, times(1)).findById(anyLong()); + verify(memberService, times(1)).getMemberById(anyLong()); verify(productRepository, times(0)).findById(anyLong()); verify(starRepository, times(0)).save(any(Star.class)); } @@ -187,8 +226,8 @@ void register_productNotFoundException() { StarRegisterRequestDto request = new StarRegisterRequestDto(1L, productId, score); - given(memberRepository.findById(anyLong())) - .willReturn(Optional.ofNullable(member)); + given(memberService.getMemberById(anyLong())) + .willReturn(member); given(reservationProductRepository.findByIdWithRoom(anyLong())) .willReturn(Optional.ofNullable(reservationProduct)); given(productRepository.findById(anyLong())) @@ -198,7 +237,7 @@ void register_productNotFoundException() { assertThatThrownBy(() -> starService.register(memberId, request)) .isInstanceOf(ProductNotFoundException.class); - verify(memberRepository, times(1)).findById(anyLong()); + verify(memberService, times(1)).getMemberById(anyLong()); verify(reservationProductRepository, times(1)).findByIdWithRoom(anyLong()); verify(productRepository, times(1)).findById(anyLong()); verify(starRepository, times(0)).save(any(Star.class)); @@ -212,18 +251,44 @@ void register_cannotBeforeCheckOutException() { long productId = 1000L; float score = 4F; StarRegisterRequestDto request - = new StarRegisterRequestDto(1L, productId, score); + = new StarRegisterRequestDto(reservationProductBeforeCheckOut.getId(), productId, score); - given(memberRepository.findById(anyLong())) - .willReturn(Optional.ofNullable(member)); + given(memberService.getMemberById(anyLong())) + .willReturn(member); given(reservationProductRepository.findByIdWithRoom(anyLong())) - .willReturn(Optional.ofNullable(reservationProduct2)); + .willReturn(Optional.ofNullable(reservationProductBeforeCheckOut)); // when & then assertThatThrownBy(() -> starService.register(memberId, request)) .isInstanceOf(CannotBeforeCheckOutException.class); - verify(memberRepository, times(1)).findById(anyLong()); + verify(memberService, times(1)).getMemberById(anyLong()); + verify(reservationProductRepository, times(1)).findByIdWithRoom(anyLong()); + verify(productRepository, times(0)).findById(anyLong()); + verify(starRepository, times(0)).save(any(Star.class)); + } + + @DisplayName("별점 등록일시가 체크아웃 일시 2주 이후일 경우 등록할 수 없다.") + @Test + void register_expiredRegisterDateException() { + // given + long memberId = member.getId(); + long productId = 1000L; + float score = 4F; + + StarRegisterRequestDto request + = new StarRegisterRequestDto(reservationProductExpiredToRegister.getId(), productId, score); + + given(memberService.getMemberById(anyLong())) + .willReturn(member); + given(reservationProductRepository.findByIdWithRoom(anyLong())) + .willReturn(Optional.ofNullable(reservationProductExpiredToRegister)); + + // when & then + assertThatThrownBy(() -> starService.register(memberId, request)) + .isInstanceOf(ExpiredRegisterDateException.class); + + verify(memberService, times(1)).getMemberById(anyLong()); verify(reservationProductRepository, times(1)).findByIdWithRoom(anyLong()); verify(productRepository, times(0)).findById(anyLong()); verify(starRepository, times(0)).save(any(Star.class)); diff --git a/src/test/resources/testdata/reservation-repository-setup.sql b/src/test/resources/testdata/reservation-repository-setup.sql deleted file mode 100644 index 5d9e17d0..00000000 --- a/src/test/resources/testdata/reservation-repository-setup.sql +++ /dev/null @@ -1,32 +0,0 @@ -insert into member (id, name, email, password, photo_url, authority) -values (1, 'member', 'member@email.com', 'password', 'photoUrl', 'ROLE_USER'); - -insert into product (id, name, category, description, star_avg, thumbnail, address) -values (1, '호텔1', 'HOTEL', '호텔1 설명', 4.1, '호텔1 사진 url', '서울시 강남구 한남동'); - -insert into product (id, name, category, description, star_avg, thumbnail, address) -values (2, '호텔2', 'HOTEL', '호텔2 설명', 3.5, '호텔2 사진 url', '서울시 송파구 잠실동'); - -insert into room (id, name, product_id, description, standard, capacity, price, check_in, check_out) -values (1, '객실1', 1, '객실1 설명', 2, 4, 95000, '13:00:00', '12:00:00'); - -insert into room (id, name, product_id, description, standard, capacity, price, check_in, check_out) -values (2, '객실2', 1, '객실2 설명', 2, 4, 105000, '14:00:00', '12:30:00'); - -insert into room (id, name, product_id, description, standard, capacity, price, check_in, check_out) -values (3, '객실3', 2, '객실3 설명', 3, 5, 125000, '13:00:00', '12:00:00'); - -insert into reservation (id, member_id, pay_method, total_price) -values (1, 1, 'CREDIT_CARD', 240000); - -insert into reservation (id, member_id, pay_method, total_price) -values (2, 1, 'CREDIT_CARD', 240000); - -insert into reservation_product (id, reservation_id, room_id, start_date, end_date, price) -values (1, 1, 1, '2023-12-10', '2023-12-13', 95000); - -insert into reservation_product (id, reservation_id, room_id, start_date, end_date, price) -values (2, 1, 3, '2023-12-13', '2023-12-15', 125000); - -insert into reservation_product (id, reservation_id, room_id, start_date, end_date, price) -values (3, 2, 2, '2023-12-24', '2023-12-27', 105000); diff --git a/src/test/resources/testdata/reservation-service-insert.sql b/src/test/resources/testdata/reservation-service-insert.sql deleted file mode 100644 index 64ba31bc..00000000 --- a/src/test/resources/testdata/reservation-service-insert.sql +++ /dev/null @@ -1,14 +0,0 @@ -insert into reservation (id, member_id, pay_method, total_price) -values (1, 1, 'CREDIT_CARD', 240000); - -insert into reservation (id, member_id, pay_method, total_price) -values (2, 1, 'CREDIT_CARD', 240000); - -insert into reservation_product (id, reservation_id, room_id, start_date, end_date, price) -values (1, 1, 1, '2023-12-10', '2023-12-13', 95000); - -insert into reservation_product (id, reservation_id, room_id, start_date, end_date, price) -values (2, 1, 3, '2023-12-13', '2023-12-15', 125000); - -insert into reservation_product (id, reservation_id, room_id, start_date, end_date, price) -values (3, 2, 2, '2023-12-24', '2023-12-27', 105000); diff --git a/src/test/resources/testdata/reservation-service-setup.sql b/src/test/resources/testdata/reservation-service-setup.sql deleted file mode 100644 index 47d69658..00000000 --- a/src/test/resources/testdata/reservation-service-setup.sql +++ /dev/null @@ -1,17 +0,0 @@ -insert into member (id, name, email, password, photo_url, authority) -values (1, 'member', 'member@email.com', 'password', 'photoUrl', 'ROLE_USER'); - -insert into product (id, name, category, description, star_avg, thumbnail, address) -values (1, '호텔1', 'HOTEL', '호텔1 설명', 4.1, '호텔1 사진 url', '서울시 강남구 한남동'); - -insert into product (id, name, category, description, star_avg, thumbnail, address) -values (2, '호텔2', 'HOTEL', '호텔2 설명', 3.5, '호텔2 사진 url', '서울시 송파구 잠실동'); - -insert into room (id, name, product_id, description, standard, capacity, price, check_in, check_out) -values (1, '객실1', 1, '객실1 설명', 2, 4, 95000, '13:00:00', '12:00:00'); - -insert into room (id, name, product_id, description, standard, capacity, price, check_in, check_out) -values (2, '객실2', 1, '객실2 설명', 2, 4, 105000, '14:00:00', '12:30:00'); - -insert into room (id, name, product_id, description, standard, capacity, price, check_in, check_out) -values (3, '객실3', 2, '객실3 설명', 3, 5, 125000, '13:00:00', '12:00:00');