From bb179e5527cd113d3f0bfd686a691c20467b27de Mon Sep 17 00:00:00 2001 From: yurim0628 Date: Wed, 31 Jan 2024 16:21:24 +0900 Subject: [PATCH 1/5] =?UTF-8?q?docs:=20=EC=95=BC=EB=86=80=EC=9E=90=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EC=83=81=ED=92=88=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20api=20=EB=AA=85=EC=84=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/product.adoc | 25 ++ .../security/SecurityConfiguration.java | 1 + .../product/service/ProductService.java | 16 +- .../controller/MockReservationController.java | 86 +++++ src/main/resources/application-test.yml | 26 ++ src/main/resources/application.yml | 6 + .../controller/ProductControllerTest.java | 357 +++++++++--------- 7 files changed, 330 insertions(+), 187 deletions(-) create mode 100644 src/main/java/site/goldenticket/dummy/reservation/controller/MockReservationController.java create mode 100644 src/main/resources/application-test.yml diff --git a/src/docs/asciidoc/product.adoc b/src/docs/asciidoc/product.adoc index 80e6bf5c..a6bd4fc3 100644 --- a/src/docs/asciidoc/product.adoc +++ b/src/docs/asciidoc/product.adoc @@ -1,3 +1,28 @@ +== 예약 목록 조회 + +=== HTTP request +include::{snippets}/reservations-all/http-request.adoc[] + +=== HTTP response fields +include::{snippets}/reservations-all/response-fields.adoc[] + +=== HTTP response +include::{snippets}/reservations-all/http-response.adoc[] + +== 상품 등록 + +=== HTTP request fields +include::{snippets}/product-create/request-fields.adoc[] + +=== HTTP request +include::{snippets}/product-create/http-request.adoc[] + +=== HTTP response fields +include::{snippets}/product-create/response-fields.adoc[] + +=== HTTP response +include::{snippets}/product-create/http-response.adoc[] + == 상품 상세 조회 === HTTP request diff --git a/src/main/java/site/goldenticket/common/security/SecurityConfiguration.java b/src/main/java/site/goldenticket/common/security/SecurityConfiguration.java index 19f2fe5c..c7cb2fb0 100644 --- a/src/main/java/site/goldenticket/common/security/SecurityConfiguration.java +++ b/src/main/java/site/goldenticket/common/security/SecurityConfiguration.java @@ -42,6 +42,7 @@ public class SecurityConfiguration { private static final String[] PERMIT_ALL_URLS = new String[]{ "/h2-console/**", "/dummy/**", + "/test/**", "/payments/**", "/home", "/chats/test/**" diff --git a/src/main/java/site/goldenticket/domain/product/service/ProductService.java b/src/main/java/site/goldenticket/domain/product/service/ProductService.java index 36cd2744..627b95eb 100644 --- a/src/main/java/site/goldenticket/domain/product/service/ProductService.java +++ b/src/main/java/site/goldenticket/domain/product/service/ProductService.java @@ -5,6 +5,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -36,7 +37,6 @@ import static site.goldenticket.common.redis.constants.RedisConstants.*; import static site.goldenticket.common.response.ErrorCode.*; -import static site.goldenticket.domain.product.constants.DummyUrlConstants.*; import static site.goldenticket.dummy.reservation.constants.ReservationStatus.NOT_REGISTERED; import static site.goldenticket.dummy.reservation.constants.ReservationStatus.REGISTERED; @@ -45,8 +45,14 @@ @RequiredArgsConstructor public class ProductService { - private final RestTemplateService restTemplateService; + @Value("${yanolja.url.base}") + private String baseUrl; + @Value("${yanolja.url.reservations}") + private String reservationsEndpoint; + @Value("${yanolja.url.reservation}") + private String reservationEndpoint; + private final RestTemplateService restTemplateService; private final RedisService redisService; private final ProductRepository productRepository; private final AlertService alertService; @@ -107,7 +113,7 @@ public Slice getProductsByAreaCode( // 2. 야놀자 예약 정보 조회 메서드 @Transactional public List getAllReservations(Long yaUserId) { - String getUrl = buildReservationUrl(RESERVATIONS_ENDPOINT, yaUserId); + String getUrl = buildReservationUrl(reservationsEndpoint, yaUserId); List yanoljaProductResponses = restTemplateService.getList( getUrl, @@ -143,7 +149,7 @@ public ProductResponse createProduct( throw new CustomException(PRODUCT_ALREADY_EXISTS); } - String getUrl = buildReservationUrl(RESERVATION_ENDPOINT, reservationId); + String getUrl = buildReservationUrl(reservationEndpoint, reservationId); ReservationDetailsResponse reservationDetailsResponse = restTemplateService.get( getUrl, @@ -202,7 +208,7 @@ public Product save(Product product) { private String buildReservationUrl(String endpoint, Long pathVariable) { return UriComponentsBuilder - .fromUriString(DISTRIBUTE_BASE_URL) + .fromUriString(baseUrl) .path(endpoint) .buildAndExpand(pathVariable) .encode(StandardCharsets.UTF_8) diff --git a/src/main/java/site/goldenticket/dummy/reservation/controller/MockReservationController.java b/src/main/java/site/goldenticket/dummy/reservation/controller/MockReservationController.java new file mode 100644 index 00000000..f77737d7 --- /dev/null +++ b/src/main/java/site/goldenticket/dummy/reservation/controller/MockReservationController.java @@ -0,0 +1,86 @@ +package site.goldenticket.dummy.reservation.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import site.goldenticket.domain.product.constants.AreaCode; +import site.goldenticket.dummy.reservation.constants.ReservationType; +import site.goldenticket.dummy.reservation.dto.ReservationDetailsResponse; +import site.goldenticket.dummy.reservation.dto.YanoljaProductResponse; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Arrays; +import java.util.List; + +@RestController +public class MockReservationController { + + @GetMapping( + path = "/test/dummy/reservations/{yaUserId}" + ) + public List getReservations( + @PathVariable Long yaUserId + ) { + return Arrays.asList( + new YanoljaProductResponse( + 1L, + "숙소명1", + ReservationType.STAY, + "객실명1", + 2, + 4, + LocalDate.of(2024, 2, 1), + LocalDate.of(2024, 2, 7), + LocalTime.of(14, 0), + LocalTime.of(12, 0), + 6, + LocalDate.now(), + 200000, + 180000 + ), + new YanoljaProductResponse( + 2L, + "숙소명2", + ReservationType.STAY, + "객실명2", + 3, + 6, + LocalDate.of(2024, 2, 2), + LocalDate.of(2024, 2, 8), + LocalTime.of(15, 0), + LocalTime.of(11, 0), + 6, + LocalDate.now(), + 250000, + 220000 + ) + ); + } + + @GetMapping( + path = "/test/dummy/reservation/{reservationId}" + ) + public ReservationDetailsResponse getReservationDetails( + @PathVariable Long reservationId + ) { + return new ReservationDetailsResponse( + 1L, + AreaCode.SEOUL, + "숙소 이미지", + "숙소명", + "숙소 주소", + ReservationType.STAY, + "객실명", + 2, + 4, + LocalTime.of(14, 0), + LocalTime.of(12, 0), + LocalDate.of(2024, 2, 1), + LocalDate.of(2024, 2, 7), + LocalDate.now(), + 200000, + 180000 + ); + } +} diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 00000000..50fe8ef8 --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,26 @@ +server: + port: 8888 + +spring: + datasource: + url: jdbc:h2:mem:test;MODE=MYSQL + username: sa + password: + driver-class-name: org.h2.Driver + mail: + host: smtp.gmail.com + port: 587 + properties: + mail.smtp.auth: true + mail.smtp.starttls.enable: true + +jwt: + secret: VTNCeWFXNW5JRk5sWTNWeWFYUjVJRWR2YkdSbGJpQlVhV05yWlhR + grant-type: Bearer + token-validate-in-seconds: 3600 + +yanolja: + url: + base: http://localhost:8888 + reservations: /test/dummy/reservations/{yaUserId} + reservation: /test/dummy/reservation/{reservationId} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4048824b..09d76ff0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -35,3 +35,9 @@ jwt: secret: VTNCeWFXNW5JRk5sWTNWeWFYUjVJRWR2YkdSbGJpQlVhV05yWlhR grant-type: Bearer token-validate-in-seconds: 3600 + +yanolja: + url: + base: http://localhost:8080 + reservations: /dummy/reservations/{yaUserId} + reservation: /dummy/reservation/{reservationId} diff --git a/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java b/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java index 3fe6a9ef..31ff47d6 100644 --- a/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java +++ b/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java @@ -7,7 +7,10 @@ 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.context.SpringBootTest.WebEnvironment; import org.springframework.http.HttpStatus; +import org.springframework.test.context.TestPropertySource; import site.goldenticket.common.config.ApiDocumentation; import site.goldenticket.domain.chat.entity.Chat; import site.goldenticket.domain.chat.entity.ChatRoom; @@ -28,8 +31,6 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.restdocs.payload.JsonFieldType.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static site.goldenticket.common.utils.ChatRoomUtils.createChatRoom; import static site.goldenticket.common.utils.ChatUtils.createChat; @@ -41,23 +42,111 @@ import static site.goldenticket.domain.product.constants.ProductStatus.EXPIRED; import static site.goldenticket.domain.product.constants.ProductStatus.SOLD_OUT; +@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT) +@TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) public class ProductControllerTest extends ApiDocumentation { @Autowired private ProductRepository productRepository; - @Autowired private ChatRoomRepository chatRoomRepository; - @Autowired private ChatRepository chatRepository; - @Autowired private NegoRepository negoRepository; - @Autowired private OrderRepository orderRepository; + @Test + @DisplayName("예약 목록 조회") + void getAllReservations() { + // given + String url = "/products/reservations/{yaUserId}"; + + // when + ExtractableResponse response = RestAssured + .given(spec).log().all() + .header("Authorization", "Bearer " + accessToken) + .filter(document( + "reservations-all", + getDocumentResponse(), + responseFields( + fieldWithPath("status").type(STRING).description("응답 상태"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data[].reservationId").type(NUMBER).description("예약 ID"), + fieldWithPath("data[].accommodationName").type(STRING).description("숙소명"), + fieldWithPath("data[].reservationType").type(STRING).description("예약 유형"), + fieldWithPath("data[].roomName").type(STRING).description("객실명"), + fieldWithPath("data[].standardNumber").type(NUMBER).description("기준 숙박 인원"), + fieldWithPath("data[].maximumNumber").type(NUMBER).description("최대 숙박 인원"), + fieldWithPath("data[].checkInDate").type(STRING).description("체크인 날짜"), + fieldWithPath("data[].checkOutDate").type(STRING).description("체크아웃 날짜"), + fieldWithPath("data[].checkInTime").type(STRING).description("체크인 시간"), + fieldWithPath("data[].checkOutTime").type(STRING).description("체크아웃 시간"), + fieldWithPath("data[].nights").type(NUMBER).description("숙박 일수"), + fieldWithPath("data[].reservationDate").type(STRING).description("예약일"), + fieldWithPath("data[].originPrice").type(NUMBER).description("구매가"), + fieldWithPath("data[].yanoljaPrice").type(NUMBER).description("야놀자 판매가"), + fieldWithPath("data[].reservationStatus").type(STRING).description("예약 상태") + ) + )) + .when() + .get(url, 1L) + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + @Test + @DisplayName("상품 등록") + void registerProduct() { + // given + ProductRequest request = createProductRequest(); + String url = "/products/{reservationId}"; + + // when + ExtractableResponse response = RestAssured + .given(spec).log().all() + .contentType(APPLICATION_JSON_VALUE) + .body(request) + .header("Authorization", "Bearer " + accessToken) + .filter(document( + "product-create", + getDocumentRequest(), + getDocumentResponse(), + requestFields( + fieldWithPath("goldenPrice").type(NUMBER).description("골든 특가"), + fieldWithPath("content").type(STRING).description("판매자 한마디") + ), + responseFields( + fieldWithPath("status").type(STRING).description("응답 상태"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data.productId").type(NUMBER).description("상품 ID"), + fieldWithPath("data.accommodationImage").type(STRING).description("숙소 이미지 URL"), + fieldWithPath("data.accommodationName").type(STRING).description("숙소명"), + fieldWithPath("data.reservationType").type(STRING).description("예약 유형"), + fieldWithPath("data.roomName").type(STRING).description("객실명"), + fieldWithPath("data.checkInDate").type(STRING).description("체크인 날짜"), + fieldWithPath("data.checkOutDate").type(STRING).description("체크아웃 날짜"), + fieldWithPath("data.nights").type(NUMBER).description("숙박 일수"), + fieldWithPath("data.days").type(NUMBER).description("판매 가능한 남은 날짜"), + fieldWithPath("data.originPrice").type(NUMBER).description("구매가"), + fieldWithPath("data.yanoljaPrice").type(NUMBER).description("야놀자 판매가"), + fieldWithPath("data.goldenPrice").type(NUMBER).description("골든 특가"), + fieldWithPath("data.productStatus").type(STRING).description("상품 상태") + ) + )) + .when() + .post(url, 1L) + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + @Test @DisplayName("상품 상세 조회") void getProduct() { @@ -65,76 +154,44 @@ void getProduct() { Product product = saveProduct(); String url = "/products/{productId}"; - String pathName = "productId"; - Long pathValues = product.getId(); // when ExtractableResponse response = RestAssured .given(spec).log().all() - .pathParam(pathName, pathValues) .filter(document( "products-details", getDocumentResponse(), - pathParameters( - parameterWithName("productId").description("상품 ID") - ), responseFields( - fieldWithPath("status").type(STRING) - .description("응답 상태"), - fieldWithPath("message").type(STRING) - .description("응답 메시지"), - fieldWithPath("data").type(OBJECT) - .description("응답 데이터"), - fieldWithPath("data.accommodationImage").type(STRING) - .description("숙소 이미지 URL"), - fieldWithPath("data.accommodationName").type(STRING) - .description("숙소명"), - fieldWithPath("data.accommodationAddress").type(STRING) - .description("숙소 주소"), - fieldWithPath("data.reservationType").type(STRING) - .description("예약 유형"), - fieldWithPath("data.roomName").type(STRING) - .description("객실명"), - fieldWithPath("data.standardNumber").type(NUMBER) - .description("기준 숙박 인원"), - fieldWithPath("data.maximumNumber").type(NUMBER) - .description("최대 숙박 인원"), - fieldWithPath("data.checkInTime").type(STRING) - .description("체크인 시간"), - fieldWithPath("data.checkOutTime").type(STRING) - .description("체크아웃 시간"), - fieldWithPath("data.checkInDate").type(STRING) - .description("체크인 날짜"), - fieldWithPath("data.checkOutDate").type(STRING) - .description("체크아웃 날짜"), - fieldWithPath("data.nights").type(NUMBER) - .description("숙박 일수"), - fieldWithPath("data.days").type(NUMBER) - .description("판매 가능한 남은 날짜"), - fieldWithPath("data.originPrice").type(NUMBER) - .description("구매가"), - fieldWithPath("data.yanoljaPrice").type(NUMBER) - .description("야놀자 판매가"), - fieldWithPath("data.goldenPrice").type(NUMBER) - .description("골든 특가"), - fieldWithPath("data.originPriceRatio").type(NUMBER) - .description("구매 가격 할인율"), - fieldWithPath("data.marketPriceRatio").type(NUMBER) - .description("야놀자 가격 할인율"), - fieldWithPath("data.content").type(STRING) - .description("판매자 한마디"), - fieldWithPath("data.productStatus").type(STRING) - .description("상품 상태"), - fieldWithPath("data.isSeller").type(BOOLEAN) - .description("판매자 여부"), - fieldWithPath("data.negoProductStatus").type(NUMBER) - .description("상품 네고 상태").optional(), - fieldWithPath("data.isWished").type(BOOLEAN) - .description("관심 상품 여부") + fieldWithPath("status").type(STRING).description("응답 상태"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(OBJECT).description("응답 데이터"), + fieldWithPath("data.accommodationImage").type(STRING).description("숙소 이미지 URL"), + fieldWithPath("data.accommodationName").type(STRING).description("숙소명"), + fieldWithPath("data.accommodationAddress").type(STRING).description("숙소 주소"), + fieldWithPath("data.reservationType").type(STRING).description("예약 유형"), + fieldWithPath("data.roomName").type(STRING).description("객실명"), + fieldWithPath("data.standardNumber").type(NUMBER).description("기준 숙박 인원"), + fieldWithPath("data.maximumNumber").type(NUMBER).description("최대 숙박 인원"), + fieldWithPath("data.checkInTime").type(STRING).description("체크인 시간"), + fieldWithPath("data.checkOutTime").type(STRING).description("체크아웃 시간"), + fieldWithPath("data.checkInDate").type(STRING).description("체크인 날짜"), + fieldWithPath("data.checkOutDate").type(STRING).description("체크아웃 날짜"), + fieldWithPath("data.nights").type(NUMBER).description("숙박 일수"), + fieldWithPath("data.days").type(NUMBER).description("판매 가능한 남은 날짜"), + fieldWithPath("data.originPrice").type(NUMBER).description("구매가"), + fieldWithPath("data.yanoljaPrice").type(NUMBER).description("야놀자 판매가"), + fieldWithPath("data.goldenPrice").type(NUMBER).description("골든 특가"), + fieldWithPath("data.originPriceRatio").type(NUMBER).description("구매 가격 할인율"), + fieldWithPath("data.marketPriceRatio").type(NUMBER).description("야놀자 가격 할인율"), + fieldWithPath("data.content").type(STRING).description("판매자 한마디"), + fieldWithPath("data.productStatus").type(STRING).description("상품 상태"), + fieldWithPath("data.isSeller").type(BOOLEAN).description("판매자 여부"), + fieldWithPath("data.negoProductStatus").type(NUMBER).description("상품 네고 상태").optional(), + fieldWithPath("data.isWished").type(BOOLEAN).description("관심 상품 여부") ) )) .when() - .get(url, pathValues) + .get(url, product.getId()) .then().log().all() .extract(); @@ -146,18 +203,14 @@ void getProduct() { @DisplayName("상품 수정") void updateProduct() { // given - ProductRequest request = createProductRequest(); - Product product = saveProduct(); + ProductRequest request = createProductRequest(); String url = "/products/{productId}"; - String pathName = "productId"; - Long pathValues = product.getId(); // when ExtractableResponse response = RestAssured .given(spec).log().all() - .pathParam(pathName, pathValues) .contentType(APPLICATION_JSON_VALUE) .body(request) .header("Authorization", "Bearer " + accessToken) @@ -165,26 +218,18 @@ void updateProduct() { "products-update", getDocumentRequest(), getDocumentResponse(), - pathParameters( - parameterWithName("productId").description("상품 ID") - ), requestFields( - fieldWithPath("goldenPrice").type(NUMBER) - .description("골든 특가"), - fieldWithPath("content").type(STRING) - .description("판매자 한마디") + fieldWithPath("goldenPrice").type(NUMBER).description("골든 특가"), + fieldWithPath("content").type(STRING).description("판매자 한마디") ), responseFields( - fieldWithPath("status").type(STRING) - .description("응답 상태"), - fieldWithPath("message").type(STRING) - .description("응답 메시지"), - fieldWithPath("data").type(NUMBER) - .description("상품 ID") + fieldWithPath("status").type(STRING).description("응답 상태"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(NUMBER).description("상품 ID") ) )) .when() - .put(url, pathValues) + .put(url, product.getId()) .then().log().all() .extract(); @@ -199,31 +244,22 @@ void deleteProduct() { Product product = saveProduct(); String url = "/products/{productId}"; - String pathName = "productId"; - Long pathValues = product.getId(); // when ExtractableResponse response = RestAssured .given(spec).log().all() - .pathParam(pathName, pathValues) .header("Authorization", "Bearer " + accessToken) .filter(document( "products-delete", getDocumentResponse(), - pathParameters( - parameterWithName("productId").description("상품 ID") - ), responseFields( - fieldWithPath("status").type(STRING) - .description("응답 상태"), - fieldWithPath("message").type(STRING) - .description("응답 메시지"), - fieldWithPath("data").type(NUMBER) - .description("상품 ID") + fieldWithPath("status").type(STRING).description("응답 상태"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(NUMBER).description("상품 ID") ) )) .when() - .delete(url, pathValues) + .delete(url, product.getId()) .then().log().all() .extract(); @@ -250,56 +286,31 @@ void getProgressProducts() { "products/history/progress-all", getDocumentResponse(), responseFields( - fieldWithPath("status").type(STRING) - .description("응답 상태"), - fieldWithPath("message").type(STRING) - .description("응답 메시지"), - fieldWithPath("data").type(ARRAY) - .description("응답 데이터"), - fieldWithPath("data[].productId").type(NUMBER) - .description("상품 ID"), - fieldWithPath("data[].accommodationImage").type(STRING) - .description("숙소 이미지 URL"), - fieldWithPath("data[].accommodationName").type(STRING) - .description("숙소명"), - fieldWithPath("data[].reservationType").type(STRING) - .description("예약 유형"), - fieldWithPath("data[].roomName").type(STRING) - .description("객실명"), - fieldWithPath("data[].standardNumber").type(NUMBER) - .description("기준 숙박 인원"), - fieldWithPath("data[].maximumNumber").type(NUMBER) - .description("최대 숙박 인원"), - fieldWithPath("data[].checkInTime").type(STRING) - .description("체크인 시간"), - fieldWithPath("data[].checkOutTime").type(STRING) - .description("체크아웃 시간"), - fieldWithPath("data[].checkInDate").type(STRING) - .description("체크인 날짜"), - fieldWithPath("data[].checkOutDate").type(STRING) - .description("체크아웃 날짜"), - fieldWithPath("data[].originPrice").type(NUMBER) - .description("구매가"), - fieldWithPath("data[].yanoljaPrice").type(NUMBER) - .description("야놀자 판매가"), - fieldWithPath("data[].goldenPrice").type(NUMBER) - .description("골든 특가"), - fieldWithPath("data[].status").type(STRING) - .description("판매 상태"), - fieldWithPath("data[].chats").type(ARRAY) - .description("채팅 목록"), - fieldWithPath("data[].chats[].chatRoomId").type(NUMBER) - .description("채팅 룸 ID"), - fieldWithPath("data[].chats[].receiverNickname").type(STRING) - .description("구매자 닉네임"), - fieldWithPath("data[].chats[].receiverProfileImage").type(STRING) - .description("구매자 프로필 이미지 경로").optional(), - fieldWithPath("data[].chats[].price").type(NUMBER) - .description("거래 가격"), - fieldWithPath("data[].chats[].chatRoomStatus").type(STRING) - .description("채팅 상태"), - fieldWithPath("data[].chats[].lastUpdatedAt").type(STRING) - .description("채팅 최근 업데이트 시간") + fieldWithPath("status").type(STRING).description("응답 상태"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(ARRAY).description("응답 데이터"), + fieldWithPath("data[].productId").type(NUMBER).description("상품 ID"), + fieldWithPath("data[].accommodationImage").type(STRING).description("숙소 이미지 URL"), + fieldWithPath("data[].accommodationName").type(STRING).description("숙소명"), + fieldWithPath("data[].reservationType").type(STRING).description("예약 유형"), + fieldWithPath("data[].roomName").type(STRING).description("객실명"), + fieldWithPath("data[].standardNumber").type(NUMBER).description("기준 숙박 인원"), + fieldWithPath("data[].maximumNumber").type(NUMBER).description("최대 숙박 인원"), + fieldWithPath("data[].checkInTime").type(STRING).description("체크인 시간"), + fieldWithPath("data[].checkOutTime").type(STRING).description("체크아웃 시간"), + fieldWithPath("data[].checkInDate").type(STRING).description("체크인 날짜"), + fieldWithPath("data[].checkOutDate").type(STRING).description("체크아웃 날짜"), + fieldWithPath("data[].originPrice").type(NUMBER).description("구매가"), + fieldWithPath("data[].yanoljaPrice").type(NUMBER).description("야놀자 판매가"), + fieldWithPath("data[].goldenPrice").type(NUMBER).description("골든 특가"), + fieldWithPath("data[].status").type(STRING).description("판매 상태"), + fieldWithPath("data[].chats").type(ARRAY).description("채팅 목록"), + fieldWithPath("data[].chats[].chatRoomId").type(NUMBER).description("채팅 룸 ID"), + fieldWithPath("data[].chats[].receiverNickname").type(STRING).description("구매자 닉네임"), + fieldWithPath("data[].chats[].receiverProfileImage").type(STRING).description("구매자 프로필 이미지 경로").optional(), + fieldWithPath("data[].chats[].price").type(NUMBER).description("거래 가격"), + fieldWithPath("data[].chats[].chatRoomStatus").type(STRING).description("채팅 상태"), + fieldWithPath("data[].chats[].lastUpdatedAt").type(STRING).description("채팅 최근 업데이트 시간") ) )) .when() @@ -327,28 +338,17 @@ void getAllCompletedProducts() { "products/history/completed-all", getDocumentResponse(), responseFields( - fieldWithPath("status").type(STRING) - .description("응답 상태"), - fieldWithPath("message").type(STRING) - .description("응답 메시지"), - fieldWithPath("data").type(ARRAY) - .description("응답 데이터"), - fieldWithPath("data[].productId").type(NUMBER) - .description("상품 ID"), - fieldWithPath("data[].accommodationImage").type(STRING) - .description("숙소 이미지 URL"), - fieldWithPath("data[].accommodationName").type(STRING) - .description("숙소명"), - fieldWithPath("data[].roomName").type(STRING) - .description("객실명"), - fieldWithPath("data[].standardNumber").type(NUMBER) - .description("기준 숙박 인원"), - fieldWithPath("data[].maximumNumber").type(NUMBER) - .description("최대 숙박 인원"), - fieldWithPath("data[].goldenPrice").type(NUMBER) - .description("골든 특가"), - fieldWithPath("data[].productStatus").type(STRING) - .description("판매 상태") + fieldWithPath("status").type(STRING).description("응답 상태"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(ARRAY).description("응답 데이터"), + fieldWithPath("data[].productId").type(NUMBER).description("상품 ID"), + fieldWithPath("data[].accommodationImage").type(STRING).description("숙소 이미지 URL"), + fieldWithPath("data[].accommodationName").type(STRING).description("숙소명"), + fieldWithPath("data[].roomName").type(STRING).description("객실명"), + fieldWithPath("data[].standardNumber").type(NUMBER).description("기준 숙박 인원"), + fieldWithPath("data[].maximumNumber").type(NUMBER).description("최대 숙박 인원"), + fieldWithPath("data[].goldenPrice").type(NUMBER).description("골든 특가"), + fieldWithPath("data[].productStatus").type(STRING).description("판매 상태") ) )) .when() @@ -361,6 +361,7 @@ void getAllCompletedProducts() { } @Test + @DisplayName("판매 내역 - 판매 완료 상세 조회") void getCompletedProductDetails() { // given Product product = saveSoldOutProduct(); @@ -388,31 +389,23 @@ void deleteCompletedProduct(){ Product product = saveSoldOutProduct(); String url = "/products/history/completed/{productId}"; - String pathName = "productId"; - Long pathValues = product.getId(); // when ExtractableResponse response = RestAssured .given(spec).log().all() - .pathParam(pathName, pathValues) .header("Authorization", "Bearer " + accessToken) .filter(document( "products/history/completed-delete", getDocumentResponse(), - pathParameters( - parameterWithName("productId").description("상품 ID") - ), + responseFields( - fieldWithPath("status").type(STRING) - .description("응답 상태"), - fieldWithPath("message").type(STRING) - .description("응답 메시지"), - fieldWithPath("data").type(NUMBER) - .description("상품 ID") + fieldWithPath("status").type(STRING).description("응답 상태"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(NUMBER).description("상품 ID") ) )) .when() - .delete(url, pathValues) + .delete(url, product.getId()) .then().log().all() .extract(); From a651df3f746efa4b76d6ac21262790a365edd311 Mon Sep 17 00:00:00 2001 From: cyPark95 Date: Thu, 1 Feb 2024 01:35:03 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=EB=8D=94=EB=AF=B8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/service/ApiUrlProperties.java | 11 +++ .../product/service/ProductService.java | 16 ++-- .../domain/user/dto/JoinRequest.java | 2 +- .../controller/MockReservationController.java | 86 ------------------- .../service/ReservationService.java | 35 +------- .../service/ReservationServiceImpl.java | 46 ++++++++++ .../goldenticket/common/config/ApiTest.java | 2 + .../controller/ProductControllerTest.java | 7 +- .../mock/FakeReservationService.java | 82 ++++++++++++++++++ .../resources/application.yml} | 30 +++++-- 10 files changed, 173 insertions(+), 144 deletions(-) create mode 100644 src/main/java/site/goldenticket/domain/product/service/ApiUrlProperties.java delete mode 100644 src/main/java/site/goldenticket/dummy/reservation/controller/MockReservationController.java create mode 100644 src/main/java/site/goldenticket/dummy/reservation/service/ReservationServiceImpl.java create mode 100644 src/test/java/site/goldenticket/mock/FakeReservationService.java rename src/{main/resources/application-test.yml => test/resources/application.yml} (53%) diff --git a/src/main/java/site/goldenticket/domain/product/service/ApiUrlProperties.java b/src/main/java/site/goldenticket/domain/product/service/ApiUrlProperties.java new file mode 100644 index 00000000..d077a68d --- /dev/null +++ b/src/main/java/site/goldenticket/domain/product/service/ApiUrlProperties.java @@ -0,0 +1,11 @@ +package site.goldenticket.domain.product.service; + +import org.springframework.stereotype.Component; + +@Component +public class ApiUrlProperties { + + public String getYanoljaUrl() { + return System.getProperty("yanolja.url"); + } +} diff --git a/src/main/java/site/goldenticket/domain/product/service/ProductService.java b/src/main/java/site/goldenticket/domain/product/service/ProductService.java index 627b95eb..cdf7648e 100644 --- a/src/main/java/site/goldenticket/domain/product/service/ProductService.java +++ b/src/main/java/site/goldenticket/domain/product/service/ProductService.java @@ -5,7 +5,6 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -45,17 +44,14 @@ @RequiredArgsConstructor public class ProductService { - @Value("${yanolja.url.base}") - private String baseUrl; - @Value("${yanolja.url.reservations}") - private String reservationsEndpoint; - @Value("${yanolja.url.reservation}") - private String reservationEndpoint; + private static final String RESERVATIONS_ENDPOINT = "/dummy/reservations/{yaUserId}"; + private static final String RESERVATION_ENDPOINT = "/dummy/reservation/{reservationId}"; private final RestTemplateService restTemplateService; private final RedisService redisService; private final ProductRepository productRepository; private final AlertService alertService; + private final ApiUrlProperties properties; // 1. 키워드 검색 및 지역 검색 메서드 @Transactional(readOnly = true) @@ -113,7 +109,7 @@ public Slice getProductsByAreaCode( // 2. 야놀자 예약 정보 조회 메서드 @Transactional public List getAllReservations(Long yaUserId) { - String getUrl = buildReservationUrl(reservationsEndpoint, yaUserId); + String getUrl = buildReservationUrl(RESERVATIONS_ENDPOINT, yaUserId); List yanoljaProductResponses = restTemplateService.getList( getUrl, @@ -149,7 +145,7 @@ public ProductResponse createProduct( throw new CustomException(PRODUCT_ALREADY_EXISTS); } - String getUrl = buildReservationUrl(reservationEndpoint, reservationId); + String getUrl = buildReservationUrl(RESERVATION_ENDPOINT, reservationId); ReservationDetailsResponse reservationDetailsResponse = restTemplateService.get( getUrl, @@ -208,7 +204,7 @@ public Product save(Product product) { private String buildReservationUrl(String endpoint, Long pathVariable) { return UriComponentsBuilder - .fromUriString(baseUrl) + .fromUriString(properties.getYanoljaUrl()) .path(endpoint) .buildAndExpand(pathVariable) .encode(StandardCharsets.UTF_8) diff --git a/src/main/java/site/goldenticket/domain/user/dto/JoinRequest.java b/src/main/java/site/goldenticket/domain/user/dto/JoinRequest.java index 5d502a40..be97166d 100644 --- a/src/main/java/site/goldenticket/domain/user/dto/JoinRequest.java +++ b/src/main/java/site/goldenticket/domain/user/dto/JoinRequest.java @@ -6,7 +6,7 @@ import site.goldenticket.domain.user.entity.User; public record JoinRequest( - @NotEmpty(message = "비밀번호는 필수 입력 항목입니다.") + @NotEmpty(message = "이름은 필수 입력 항목입니다.") @Size(min = 2, message = "이름은 두 글자 이상의 한글이어야 합니다.") String name, @NotEmpty(message = "닉네임은 필수 입력 항목입니다.") diff --git a/src/main/java/site/goldenticket/dummy/reservation/controller/MockReservationController.java b/src/main/java/site/goldenticket/dummy/reservation/controller/MockReservationController.java deleted file mode 100644 index f77737d7..00000000 --- a/src/main/java/site/goldenticket/dummy/reservation/controller/MockReservationController.java +++ /dev/null @@ -1,86 +0,0 @@ -package site.goldenticket.dummy.reservation.controller; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RestController; -import site.goldenticket.domain.product.constants.AreaCode; -import site.goldenticket.dummy.reservation.constants.ReservationType; -import site.goldenticket.dummy.reservation.dto.ReservationDetailsResponse; -import site.goldenticket.dummy.reservation.dto.YanoljaProductResponse; - -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.Arrays; -import java.util.List; - -@RestController -public class MockReservationController { - - @GetMapping( - path = "/test/dummy/reservations/{yaUserId}" - ) - public List getReservations( - @PathVariable Long yaUserId - ) { - return Arrays.asList( - new YanoljaProductResponse( - 1L, - "숙소명1", - ReservationType.STAY, - "객실명1", - 2, - 4, - LocalDate.of(2024, 2, 1), - LocalDate.of(2024, 2, 7), - LocalTime.of(14, 0), - LocalTime.of(12, 0), - 6, - LocalDate.now(), - 200000, - 180000 - ), - new YanoljaProductResponse( - 2L, - "숙소명2", - ReservationType.STAY, - "객실명2", - 3, - 6, - LocalDate.of(2024, 2, 2), - LocalDate.of(2024, 2, 8), - LocalTime.of(15, 0), - LocalTime.of(11, 0), - 6, - LocalDate.now(), - 250000, - 220000 - ) - ); - } - - @GetMapping( - path = "/test/dummy/reservation/{reservationId}" - ) - public ReservationDetailsResponse getReservationDetails( - @PathVariable Long reservationId - ) { - return new ReservationDetailsResponse( - 1L, - AreaCode.SEOUL, - "숙소 이미지", - "숙소명", - "숙소 주소", - ReservationType.STAY, - "객실명", - 2, - 4, - LocalTime.of(14, 0), - LocalTime.of(12, 0), - LocalDate.of(2024, 2, 1), - LocalDate.of(2024, 2, 7), - LocalDate.now(), - 200000, - 180000 - ); - } -} diff --git a/src/main/java/site/goldenticket/dummy/reservation/service/ReservationService.java b/src/main/java/site/goldenticket/dummy/reservation/service/ReservationService.java index 40346bff..9b009c4a 100644 --- a/src/main/java/site/goldenticket/dummy/reservation/service/ReservationService.java +++ b/src/main/java/site/goldenticket/dummy/reservation/service/ReservationService.java @@ -1,44 +1,15 @@ package site.goldenticket.dummy.reservation.service; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import site.goldenticket.common.exception.CustomException; import site.goldenticket.dummy.reservation.dto.ReservationDetailsResponse; import site.goldenticket.dummy.reservation.dto.YanoljaProductResponse; -import site.goldenticket.dummy.reservation.model.Reservation; -import site.goldenticket.dummy.reservation.repository.ReservationRepository; -import java.time.LocalDate; import java.util.List; -import java.util.stream.Collectors; - -import static site.goldenticket.common.response.ErrorCode.RESERVATION_NOT_FOUND; - -@Service -@RequiredArgsConstructor -@Slf4j -public class ReservationService { - private final ReservationRepository reservationRepository; +public interface ReservationService { @Transactional(readOnly = true) - public List getReservations(Long yaUserId) { - LocalDate currentDate = LocalDate.now(); - List reservationList = reservationRepository.findByYaUserIdAndCheckInDateAfter(yaUserId, currentDate); - return reservationList.stream() - .map(YanoljaProductResponse::fromEntity) - .collect(Collectors.toList()); - } + List getReservations(Long yaUserId); @Transactional(readOnly = true) - public ReservationDetailsResponse getReservationDetails (Long reservationId) { - Reservation reservation = getReservation(reservationId); - return ReservationDetailsResponse.fromEntity(reservation); - } - - public Reservation getReservation(Long reservationId) { - return reservationRepository.findById(reservationId) - .orElseThrow(() -> new CustomException(RESERVATION_NOT_FOUND)); - } + ReservationDetailsResponse getReservationDetails(Long reservationId); } diff --git a/src/main/java/site/goldenticket/dummy/reservation/service/ReservationServiceImpl.java b/src/main/java/site/goldenticket/dummy/reservation/service/ReservationServiceImpl.java new file mode 100644 index 00000000..5669bbcb --- /dev/null +++ b/src/main/java/site/goldenticket/dummy/reservation/service/ReservationServiceImpl.java @@ -0,0 +1,46 @@ +package site.goldenticket.dummy.reservation.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import site.goldenticket.common.exception.CustomException; +import site.goldenticket.dummy.reservation.dto.ReservationDetailsResponse; +import site.goldenticket.dummy.reservation.dto.YanoljaProductResponse; +import site.goldenticket.dummy.reservation.model.Reservation; +import site.goldenticket.dummy.reservation.repository.ReservationRepository; + +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Collectors; + +import static site.goldenticket.common.response.ErrorCode.RESERVATION_NOT_FOUND; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ReservationServiceImpl implements ReservationService { + private final ReservationRepository reservationRepository; + + @Override + @Transactional(readOnly = true) + public List getReservations(Long yaUserId) { + LocalDate currentDate = LocalDate.now(); + List reservationList = reservationRepository.findByYaUserIdAndCheckInDateAfter(yaUserId, currentDate); + return reservationList.stream() + .map(YanoljaProductResponse::fromEntity) + .collect(Collectors.toList()); + } + + @Override + @Transactional(readOnly = true) + public ReservationDetailsResponse getReservationDetails(Long reservationId) { + Reservation reservation = getReservation(reservationId); + return ReservationDetailsResponse.fromEntity(reservation); + } + + private Reservation getReservation(Long reservationId) { + return reservationRepository.findById(reservationId) + .orElseThrow(() -> new CustomException(RESERVATION_NOT_FOUND)); + } +} diff --git a/src/test/java/site/goldenticket/common/config/ApiTest.java b/src/test/java/site/goldenticket/common/config/ApiTest.java index 2a4fac10..07b6ee5c 100644 --- a/src/test/java/site/goldenticket/common/config/ApiTest.java +++ b/src/test/java/site/goldenticket/common/config/ApiTest.java @@ -37,6 +37,8 @@ public abstract class ApiTest { @BeforeEach void init() { RestAssured.port = port; + System.setProperty("yanolja.url", "http://localhost:" + port); + user = saveUser(); accessToken = getAccessToken(); } diff --git a/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java b/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java index 31ff47d6..a81d9961 100644 --- a/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java +++ b/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java @@ -7,10 +7,7 @@ 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.context.SpringBootTest.WebEnvironment; import org.springframework.http.HttpStatus; -import org.springframework.test.context.TestPropertySource; import site.goldenticket.common.config.ApiDocumentation; import site.goldenticket.domain.chat.entity.Chat; import site.goldenticket.domain.chat.entity.ChatRoom; @@ -42,8 +39,6 @@ import static site.goldenticket.domain.product.constants.ProductStatus.EXPIRED; import static site.goldenticket.domain.product.constants.ProductStatus.SOLD_OUT; -@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT) -@TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) public class ProductControllerTest extends ApiDocumentation { @Autowired @@ -91,7 +86,7 @@ void getAllReservations() { ) )) .when() - .get(url, 1L) + .get(url, -1L) .then().log().all() .extract(); diff --git a/src/test/java/site/goldenticket/mock/FakeReservationService.java b/src/test/java/site/goldenticket/mock/FakeReservationService.java new file mode 100644 index 00000000..49ebdbc1 --- /dev/null +++ b/src/test/java/site/goldenticket/mock/FakeReservationService.java @@ -0,0 +1,82 @@ +package site.goldenticket.mock; + +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; +import site.goldenticket.domain.product.constants.AreaCode; +import site.goldenticket.dummy.reservation.constants.ReservationType; +import site.goldenticket.dummy.reservation.dto.ReservationDetailsResponse; +import site.goldenticket.dummy.reservation.dto.YanoljaProductResponse; +import site.goldenticket.dummy.reservation.service.ReservationService; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Arrays; +import java.util.List; + +@Primary +@Service +public class FakeReservationService implements ReservationService { + + @Override + public List getReservations(Long yaUserId) { + if (yaUserId == -1L) { + return Arrays.asList( + new YanoljaProductResponse( + 1L, + "숙소명1", + ReservationType.STAY, + "객실명1", + 2, + 4, + LocalDate.of(2024, 2, 1), + LocalDate.of(2024, 2, 7), + LocalTime.of(14, 0), + LocalTime.of(12, 0), + 6, + LocalDate.now(), + 200000, + 180000 + ), + new YanoljaProductResponse( + 2L, + "숙소명2", + ReservationType.STAY, + "객실명2", + 3, + 6, + LocalDate.of(2024, 2, 2), + LocalDate.of(2024, 2, 8), + LocalTime.of(15, 0), + LocalTime.of(11, 0), + 6, + LocalDate.now(), + 250000, + 220000 + ) + ); + } + return List.of(); + } + + @Override + public ReservationDetailsResponse getReservationDetails(Long reservationId) { + return new ReservationDetailsResponse( + 1L, + AreaCode.SEOUL, + "숙소 이미지", + "숙소명", + "숙소 주소", + ReservationType.STAY, + "객실명", + 2, + 4, + LocalTime.of(14, 0), + LocalTime.of(12, 0), + LocalDate.of(2024, 2, 1), + LocalDate.of(2024, 2, 7), + LocalDate.now(), + 200000, + 180000 + ); + } +} diff --git a/src/main/resources/application-test.yml b/src/test/resources/application.yml similarity index 53% rename from src/main/resources/application-test.yml rename to src/test/resources/application.yml index 50fe8ef8..87d38dfd 100644 --- a/src/main/resources/application-test.yml +++ b/src/test/resources/application.yml @@ -1,6 +1,3 @@ -server: - port: 8888 - spring: datasource: url: jdbc:h2:mem:test;MODE=MYSQL @@ -10,17 +7,32 @@ spring: mail: host: smtp.gmail.com port: 587 + #username: ${MAIL_USER_NAME} + #password: ${MAIL_PASSWORD} properties: mail.smtp.auth: true mail.smtp.starttls.enable: true + h2: + console: + enabled: true + + jpa: + database-platform: H2 + hibernate: + ddl-auto: create-drop + properties: + hibernate: + show_sql: true + format_sql: true + +logging: + level: + org: + springframework: + security: TRACE + jwt: secret: VTNCeWFXNW5JRk5sWTNWeWFYUjVJRWR2YkdSbGJpQlVhV05yWlhR grant-type: Bearer token-validate-in-seconds: 3600 - -yanolja: - url: - base: http://localhost:8888 - reservations: /test/dummy/reservations/{yaUserId} - reservation: /test/dummy/reservation/{reservationId} From 853b887aae8f2a9cf62cbb472daff733d1528d84 Mon Sep 17 00:00:00 2001 From: yurim0628 Date: Thu, 1 Feb 2024 02:23:21 +0900 Subject: [PATCH 3/5] =?UTF-8?q?docs:=20=ED=8C=90=EB=A7=A4=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20api=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/product.adoc | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/docs/asciidoc/product.adoc b/src/docs/asciidoc/product.adoc index a6bd4fc3..a4ab81ca 100644 --- a/src/docs/asciidoc/product.adoc +++ b/src/docs/asciidoc/product.adoc @@ -70,6 +70,17 @@ include::{snippets}/products/history/progress-all/response-fields.adoc[] === HTTP response include::{snippets}/products/history/progress-all/http-response.adoc[] +== 판매내역 - 판매중 전체 조회 + +=== HTTP request +include::{snippets}/products/history/progress-all/http-request.adoc[] + +=== HTTP response fields +include::{snippets}/products/history/progress-all/response-fields.adoc[] + +=== HTTP response +include::{snippets}/products/history/progress-all/http-response.adoc[] + == 판매내역 - 판매완료 전체 조회 === HTTP request @@ -81,6 +92,17 @@ include::{snippets}/products/history/completed-all/response-fields.adoc[] === HTTP response include::{snippets}/products/history/completed-all/http-response.adoc[] +== 판매내역 - 판매완료 상세 조회 + +=== HTTP request +include::{snippets}/products/history/completed-details/http-request.adoc[] + +=== HTTP response fields +include::{snippets}/products/history/completed-details/response-fields.adoc[] + +=== HTTP response +include::{snippets}/products/history/completed-details/http-response.adoc[] + == 판매내역 - 판매완료 삭제 From 0fbac18e89c6f184d88594b9836eae6064cb458d Mon Sep 17 00:00:00 2001 From: yurim0628 Date: Thu, 1 Feb 2024 15:37:38 +0900 Subject: [PATCH 4/5] =?UTF-8?q?docs:=20=ED=82=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20api=20=EB=AA=85=EC=84=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/product.adoc | 32 +++ .../repository/ProductRepositoryImpl.java | 4 +- .../controller/ProductControllerTest.java | 204 ++++++++++++++++-- 3 files changed, 222 insertions(+), 18 deletions(-) diff --git a/src/docs/asciidoc/product.adoc b/src/docs/asciidoc/product.adoc index a4ab81ca..922658cf 100644 --- a/src/docs/asciidoc/product.adoc +++ b/src/docs/asciidoc/product.adoc @@ -1,5 +1,22 @@ +== 키워드 검색 + +=== HTTP query parameters +include::{snippets}/products-search/query-parameters.adoc[] + +=== HTTP request +include::{snippets}/products-search/http-request.adoc[] + +=== HTTP response fields +include::{snippets}/products-search/response-fields.adoc[] + +=== HTTP response +include::{snippets}/products-search/http-response.adoc[] + == 예약 목록 조회 +=== HTTP path parameters +include::{snippets}/reservations-all/path-parameters.adoc[] + === HTTP request include::{snippets}/reservations-all/http-request.adoc[] @@ -25,6 +42,9 @@ include::{snippets}/product-create/http-response.adoc[] == 상품 상세 조회 +=== HTTP path parameters +include::{snippets}/products-details/path-parameters.adoc[] + === HTTP request include::{snippets}/products-details/http-request.adoc[] @@ -36,6 +56,9 @@ include::{snippets}/products-details/http-response.adoc[] == 상품 수정 +=== HTTP path parameters +include::{snippets}/products-update/path-parameters.adoc[] + === HTTP request fields include::{snippets}/products-update/request-fields.adoc[] @@ -50,6 +73,9 @@ include::{snippets}/products-update/http-response.adoc[] == 상품 삭제 +=== HTTP path parameters +include::{snippets}/products-delete/path-parameters.adoc[] + === HTTP request include::{snippets}/products-delete/http-request.adoc[] @@ -94,6 +120,9 @@ include::{snippets}/products/history/completed-all/http-response.adoc[] == 판매내역 - 판매완료 상세 조회 +=== HTTP query parameters +include::{snippets}/products/history/completed-details/query-parameters.adoc[] + === HTTP request include::{snippets}/products/history/completed-details/http-request.adoc[] @@ -106,6 +135,9 @@ include::{snippets}/products/history/completed-details/http-response.adoc[] == 판매내역 - 판매완료 삭제 +=== HTTP path parameters +include::{snippets}/products/history/completed-delete/path-parameters.adoc[] + === HTTP request include::{snippets}/products/history/completed-delete/http-request.adoc[] diff --git a/src/main/java/site/goldenticket/domain/product/repository/ProductRepositoryImpl.java b/src/main/java/site/goldenticket/domain/product/repository/ProductRepositoryImpl.java index db9ba3ba..86f3bdb6 100644 --- a/src/main/java/site/goldenticket/domain/product/repository/ProductRepositoryImpl.java +++ b/src/main/java/site/goldenticket/domain/product/repository/ProductRepositoryImpl.java @@ -145,9 +145,7 @@ private BooleanExpression buildAccommodationNameCondition(QProduct product, Stri private BooleanExpression buildCheckInCheckOutCondition(QProduct product, LocalDate checkInDate, LocalDate checkOutDate) { return product.checkInDate.between(checkInDate, checkOutDate) - .and(product.checkOutDate.between(checkInDate, checkOutDate)) - .or(product.checkInDate.eq(checkInDate)) - .or(product.checkOutDate.eq(checkOutDate)); + .and(product.checkOutDate.between(checkInDate, checkOutDate)); } private BooleanExpression buildPriceRangeCondition(QProduct product, PriceRange priceRange) { diff --git a/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java b/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java index a81d9961..447962e0 100644 --- a/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java +++ b/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java @@ -1,7 +1,6 @@ package site.goldenticket.domain.product.controller; import io.restassured.RestAssured; -import io.restassured.path.json.JsonPath; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import org.junit.jupiter.api.DisplayName; @@ -17,6 +16,8 @@ import site.goldenticket.domain.nego.repository.NegoRepository; import site.goldenticket.domain.payment.model.Order; import site.goldenticket.domain.payment.repository.OrderRepository; +import site.goldenticket.domain.product.constants.AreaCode; +import site.goldenticket.domain.product.constants.PriceRange; import site.goldenticket.domain.product.dto.ProductRequest; import site.goldenticket.domain.product.model.Product; import site.goldenticket.domain.product.repository.ProductRepository; @@ -24,10 +25,12 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.LIST; import static org.springframework.http.HttpStatus.OK; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.restdocs.payload.JsonFieldType.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static site.goldenticket.common.utils.ChatRoomUtils.createChatRoom; import static site.goldenticket.common.utils.ChatUtils.createChat; @@ -35,10 +38,11 @@ import static site.goldenticket.common.utils.OrderUtils.createOrder; import static site.goldenticket.common.utils.ProductUtils.createProduct; import static site.goldenticket.common.utils.ProductUtils.createProductRequest; -import static site.goldenticket.common.utils.RestAssuredUtils.restAssuredGetWithTokenAndQueryParam; +import static site.goldenticket.domain.product.constants.PriceRange.BETWEEN_10_AND_20; import static site.goldenticket.domain.product.constants.ProductStatus.EXPIRED; import static site.goldenticket.domain.product.constants.ProductStatus.SOLD_OUT; +@DisplayName("ProductController 검증") public class ProductControllerTest extends ApiDocumentation { @Autowired @@ -52,11 +56,119 @@ public class ProductControllerTest extends ApiDocumentation { @Autowired private OrderRepository orderRepository; + @Test + @DisplayName("상품 검색 조회") + void getProductsBySearch() { + // given + Product product = saveProduct(); + + String url = "/products?areaCode={areaCode}&keyword={keyword}&checkInDate={checkInDate}" + + "&checkOutDate={checkOutDate}&priceRange={priceRange}&cursorId={cursorId}" + + "&cursorCheckInDate={cursorCheckInDate}"; + + AreaCode areaCode = product.getAreaCode(); + String accommodationName = product.getAccommodationName(); + String checkInDate = String.valueOf(product.getCheckInDate()); + String checkOutDate = String.valueOf(product.getCheckOutDate()); + PriceRange priceRange = BETWEEN_10_AND_20; + Long cursorId = 0L; + String cursorCheckInDate = String.valueOf(product.getCheckInDate().minusDays(1)); + + // when + ExtractableResponse response = RestAssured + .given(spec).log().all() + .filter(document( + "products-search", + getDocumentResponse(), + queryParameters( + parameterWithName("areaCode").description("지역명"), + parameterWithName("keyword").description("숙소명"), + parameterWithName("checkInDate").description("체크인 날짜"), + parameterWithName("checkOutDate").description("체크아웃 날짜"), + parameterWithName("priceRange").description("가격 범위"), + parameterWithName("cursorId").description("커서 아이디"), + parameterWithName("cursorCheckInDate").description("커서 체크인 날짜") + ), + responseFields( + fieldWithPath("status").type(STRING).description("응답 상태"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data.content").type(LIST).description("컨텐츠 리스트"), + fieldWithPath("data.pageable").type(OBJECT).description("페이징 정보"), + fieldWithPath("data.first").type(BOOLEAN).description("첫 번째 페이지 여부"), + fieldWithPath("data.last").type(BOOLEAN).description("마지막 페이지 여부"), + fieldWithPath("data.size").type(NUMBER).description("페이지 크기"), + fieldWithPath("data.number").type(NUMBER).description("페이지 번호"), + fieldWithPath("data.numberOfElements").type(NUMBER).description("현재 페이지의 요소 수"), + fieldWithPath("data.empty").type(BOOLEAN).description("데이터가 비어 있는지 여부"), + + // pageable + fieldWithPath("data.pageable.pageNumber").type(NUMBER).description("페이지 번호"), + fieldWithPath("data.pageable.pageSize").type(NUMBER).description("페이지 크기"), + fieldWithPath("data.pageable.sort").type(OBJECT).description("정렬 정보"), + fieldWithPath("data.pageable.sort.empty").type(BOOLEAN).description("정렬 정보가 비어 있는지 여부"), + fieldWithPath("data.pageable.sort.sorted").type(BOOLEAN).description("정렬 정보가 정렬되어 있는지 여부"), + fieldWithPath("data.pageable.sort.unsorted").type(BOOLEAN).description("정렬 정보가 정렬되어 있지 않은지 여부"), + fieldWithPath("data.pageable.offset").type(NUMBER).description("오프셋"), + fieldWithPath("data.pageable.paged").type(BOOLEAN).description("페이징 여부"), + fieldWithPath("data.pageable.unpaged").type(BOOLEAN).description("페이징되지 않은 경우 여부"), + + // sort fields + fieldWithPath("data.sort.empty").type(BOOLEAN).description("정렬 정보가 비어 있는지 여부"), + fieldWithPath("data.sort.sorted").type(BOOLEAN).description("정렬 정보가 정렬되어 있는지 여부"), + fieldWithPath("data.sort.unsorted").type(BOOLEAN).description("정렬 정보가 정렬되어 있지 않은지 여부"), + + // content fields + fieldWithPath("data.content[0].areaName").type(STRING).description("지역명"), + fieldWithPath("data.content[0].keyword").type(STRING).description("검색어"), + fieldWithPath("data.content[0].checkInDate").type(STRING).description("체크인 날짜"), + fieldWithPath("data.content[0].checkOutDate").type(STRING).description("체크아웃 날짜"), + fieldWithPath("data.content[0].priceRange").type(STRING).description("가격대"), + fieldWithPath("data.content[0].totalCount").type(NUMBER).description("총 개수"), + + // productResponseList + fieldWithPath("data.content[0].wishedProductResponseList").type(LIST).description("상품 응답 리스트"), + fieldWithPath("data.content[0].wishedProductResponseList[0].productId").type(NUMBER).description("상품 ID"), + fieldWithPath("data.content[0].wishedProductResponseList[0].accommodationImage").type(STRING).description("숙소 이미지"), + fieldWithPath("data.content[0].wishedProductResponseList[0].accommodationName").type(STRING).description("숙소명"), + fieldWithPath("data.content[0].wishedProductResponseList[0].reservationType").type(STRING).description("예약 유형"), + fieldWithPath("data.content[0].wishedProductResponseList[0].roomName").type(STRING).description("객실명"), + fieldWithPath("data.content[0].wishedProductResponseList[0].checkInDate").type(STRING).description("체크인 날짜"), + fieldWithPath("data.content[0].wishedProductResponseList[0].checkOutDate").type(STRING).description("체크아웃 날짜"), + fieldWithPath("data.content[0].wishedProductResponseList[0].nights").type(NUMBER).description("숙박 일수"), + fieldWithPath("data.content[0].wishedProductResponseList[0].days").type(NUMBER).description("판매 종료 까지 남은 일 수"), + fieldWithPath("data.content[0].wishedProductResponseList[0].originPrice").type(NUMBER).description("원래 가격"), + fieldWithPath("data.content[0].wishedProductResponseList[0].yanoljaPrice").type(NUMBER).description("야놀자 가격"), + fieldWithPath("data.content[0].wishedProductResponseList[0].goldenPrice").type(NUMBER).description("골든 가격"), + fieldWithPath("data.content[0].wishedProductResponseList[0].originPriceRatio").type(NUMBER).description("구매가 대비 할인율"), + fieldWithPath("data.content[0].wishedProductResponseList[0].marketPriceRatio").type(NUMBER).description("야놀자 판매가 대비 할인율"), + fieldWithPath("data.content[0].wishedProductResponseList[0].productStatus").type(STRING).description("상품 상태"), + fieldWithPath("data.content[0].wishedProductResponseList[0].isWished").type(BOOLEAN).description("찜 여부") + ) + )) + .when() + .get( + url, + areaCode, + accommodationName, + checkInDate, + checkOutDate, + priceRange, + cursorId, + cursorCheckInDate + ) + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + @Test @DisplayName("예약 목록 조회") void getAllReservations() { // given String url = "/products/reservations/{yaUserId}"; + Long yaUserId = -1L; // when ExtractableResponse response = RestAssured @@ -65,6 +177,9 @@ void getAllReservations() { .filter(document( "reservations-all", getDocumentResponse(), + pathParameters( + parameterWithName("yaUserId").description("야놀자 ID") + ), responseFields( fieldWithPath("status").type(STRING).description("응답 상태"), fieldWithPath("message").type(STRING).description("응답 메시지"), @@ -86,7 +201,7 @@ void getAllReservations() { ) )) .when() - .get(url, -1L) + .get(url, yaUserId) .then().log().all() .extract(); @@ -99,7 +214,9 @@ void getAllReservations() { void registerProduct() { // given ProductRequest request = createProductRequest(); + String url = "/products/{reservationId}"; + Long reservationId = 1L; // when ExtractableResponse response = RestAssured @@ -111,6 +228,9 @@ void registerProduct() { "product-create", getDocumentRequest(), getDocumentResponse(), + pathParameters( + parameterWithName("reservationId").description("예약 ID") + ), requestFields( fieldWithPath("goldenPrice").type(NUMBER).description("골든 특가"), fieldWithPath("content").type(STRING).description("판매자 한마디") @@ -134,7 +254,7 @@ void registerProduct() { ) )) .when() - .post(url, 1L) + .post(url, reservationId) .then().log().all() .extract(); @@ -149,6 +269,7 @@ void getProduct() { Product product = saveProduct(); String url = "/products/{productId}"; + Long productId = product.getId(); // when ExtractableResponse response = RestAssured @@ -156,6 +277,9 @@ void getProduct() { .filter(document( "products-details", getDocumentResponse(), + pathParameters( + parameterWithName("productId").description("상품 ID") + ), responseFields( fieldWithPath("status").type(STRING).description("응답 상태"), fieldWithPath("message").type(STRING).description("응답 메시지"), @@ -186,7 +310,7 @@ void getProduct() { ) )) .when() - .get(url, product.getId()) + .get(url, productId) .then().log().all() .extract(); @@ -202,6 +326,7 @@ void updateProduct() { ProductRequest request = createProductRequest(); String url = "/products/{productId}"; + Long productId = product.getId(); // when ExtractableResponse response = RestAssured @@ -213,6 +338,9 @@ void updateProduct() { "products-update", getDocumentRequest(), getDocumentResponse(), + pathParameters( + parameterWithName("productId").description("상품 ID") + ), requestFields( fieldWithPath("goldenPrice").type(NUMBER).description("골든 특가"), fieldWithPath("content").type(STRING).description("판매자 한마디") @@ -224,7 +352,7 @@ void updateProduct() { ) )) .when() - .put(url, product.getId()) + .put(url, productId) .then().log().all() .extract(); @@ -239,6 +367,7 @@ void deleteProduct() { Product product = saveProduct(); String url = "/products/{productId}"; + Long productId = product.getId(); // when ExtractableResponse response = RestAssured @@ -247,6 +376,9 @@ void deleteProduct() { .filter(document( "products-delete", getDocumentResponse(), + pathParameters( + parameterWithName("productId").description("상품 ID") + ), responseFields( fieldWithPath("status").type(STRING).description("응답 상태"), fieldWithPath("message").type(STRING).description("응답 메시지"), @@ -254,7 +386,7 @@ void deleteProduct() { ) )) .when() - .delete(url, product.getId()) + .delete(url, productId) .then().log().all() .extract(); @@ -364,17 +496,56 @@ void getCompletedProductDetails() { saveChat(chatRoom); saveOrder(product); - String url = "/products/history/completed/" + product.getId(); - String parameterName = "productStatus"; - String parameterValues = product.getProductStatus().toString(); + String url = "/products/history/completed/{productId}?productStatus={productStatus}"; + Long productId = product.getId(); + String productStatus = product.getProductStatus().toString(); // when - final ExtractableResponse response = restAssuredGetWithTokenAndQueryParam(url, parameterName, parameterValues, accessToken); + ExtractableResponse response = RestAssured + .given(spec).log().all() + .header("Authorization", "Bearer " + accessToken) + .filter(document( + "products/history/completed-details", + getDocumentResponse(), + pathParameters( + parameterWithName("productId").description("상품 ID") + ), + queryParameters( + parameterWithName("productStatus").description("상품 상태") + ), + responseFields( + fieldWithPath("status").type(STRING).description("응답 상태"), + fieldWithPath("message").type(STRING).description("응답 메시지").optional(), + fieldWithPath("data").type(OBJECT).description("응답 데이터"), + fieldWithPath("data.productId").type(NUMBER).description("상품 ID"), + fieldWithPath("data.accommodationImage").type(STRING).description("숙소 이미지 URL"), + fieldWithPath("data.accommodationName").type(STRING).description("숙소명"), + fieldWithPath("data.roomName").type(STRING).description("객실명"), + fieldWithPath("data.reservationType").type(STRING).description("예약 유형"), + fieldWithPath("data.standardNumber").type(NUMBER).description("기준 숙박 인원"), + fieldWithPath("data.maximumNumber").type(NUMBER).description("최대 숙박 인원"), + fieldWithPath("data.checkInTime").type(STRING).description("체크인 시간"), + fieldWithPath("data.checkOutTime").type(STRING).description("체크아웃 시간"), + fieldWithPath("data.checkInDate").type(STRING).description("체크인 날짜"), + fieldWithPath("data.checkOutDate").type(STRING).description("체크아웃 날짜"), + fieldWithPath("data.goldenPrice").type(NUMBER).description("골든 특가"), + fieldWithPath("data.completedDate").type(STRING).description("거래 날짜"), + fieldWithPath("data.calculatedDate").type(STRING).description("정산 날짜"), + fieldWithPath("data.fee").type(NUMBER).description("수수료"), + fieldWithPath("data.calculatedPrice").type(NUMBER).description("정산 금액"), + fieldWithPath("data.chatRoomId").type(NUMBER).description("채팅 룸 ID"), + fieldWithPath("data.receiverNickname").type(STRING).description("구매자 닉네임"), + fieldWithPath("data.receiverProfileImage").type(STRING).description("구매자 프로필 이미지 경로").optional(), + fieldWithPath("data.lastUpdatedAt").type(STRING).description("채팅 최근 업데이트 시간") + ) + )) + .when() + .get(url, productId, productStatus) + .then().log().all() + .extract(); // then assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); - final JsonPath result = response.jsonPath(); - assertThat(result.getLong("data.productId")).isEqualTo(product.getId()); } @Test @@ -384,6 +555,7 @@ void deleteCompletedProduct(){ Product product = saveSoldOutProduct(); String url = "/products/history/completed/{productId}"; + Long productId = product.getId(); // when ExtractableResponse response = RestAssured @@ -392,7 +564,9 @@ void deleteCompletedProduct(){ .filter(document( "products/history/completed-delete", getDocumentResponse(), - + pathParameters( + parameterWithName("productId").description("상품 ID") + ), responseFields( fieldWithPath("status").type(STRING).description("응답 상태"), fieldWithPath("message").type(STRING).description("응답 메시지"), @@ -400,7 +574,7 @@ void deleteCompletedProduct(){ ) )) .when() - .delete(url, product.getId()) + .delete(url, productId) .then().log().all() .extract(); From 0230aa608d56ded00434a2998a69ae5dc11af201 Mon Sep 17 00:00:00 2001 From: yurim0628 Date: Fri, 2 Feb 2024 15:39:43 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=EA=B2=80=EC=83=89=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=20=ED=95=84=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/constants/DummyUrlConstants.java | 8 --- .../product/controller/ProductController.java | 10 +-- .../product/dto/RegionProductResponse.java | 6 +- .../product/dto/SearchProductResponse.java | 6 +- .../product/repository/CustomSlice.java | 66 +------------------ .../repository/ProductRepositoryImpl.java | 4 +- .../product/service/ProductService.java | 32 ++++----- .../controller/ProductControllerTest.java | 29 ++------ 8 files changed, 31 insertions(+), 130 deletions(-) delete mode 100644 src/main/java/site/goldenticket/domain/product/constants/DummyUrlConstants.java diff --git a/src/main/java/site/goldenticket/domain/product/constants/DummyUrlConstants.java b/src/main/java/site/goldenticket/domain/product/constants/DummyUrlConstants.java deleted file mode 100644 index 2f750483..00000000 --- a/src/main/java/site/goldenticket/domain/product/constants/DummyUrlConstants.java +++ /dev/null @@ -1,8 +0,0 @@ -package site.goldenticket.domain.product.constants; - -public class DummyUrlConstants { - public static final String DISTRIBUTE_BASE_URL = "https://golden-ticket.site/"; - public static final String LOCAL_BASE_URL = "http://localhost:8080"; - public static final String RESERVATIONS_ENDPOINT = "/dummy/reservations/{yaUserId}"; - public static final String RESERVATION_ENDPOINT = "/dummy/reservation/{reservationId}"; -} diff --git a/src/main/java/site/goldenticket/domain/product/controller/ProductController.java b/src/main/java/site/goldenticket/domain/product/controller/ProductController.java index 50481539..f1dd762e 100644 --- a/src/main/java/site/goldenticket/domain/product/controller/ProductController.java +++ b/src/main/java/site/goldenticket/domain/product/controller/ProductController.java @@ -4,7 +4,6 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; @@ -19,6 +18,7 @@ import site.goldenticket.domain.product.constants.PriceRange; import site.goldenticket.domain.product.constants.ProductStatus; import site.goldenticket.domain.product.dto.*; +import site.goldenticket.domain.product.repository.CustomSlice; import site.goldenticket.domain.product.search.service.SearchService; import site.goldenticket.domain.product.service.ProductOrderService; import site.goldenticket.domain.product.service.ProductService; @@ -41,7 +41,7 @@ public class ProductController { private final ProductOrderService productOrderService; @GetMapping - public CompletableFuture>>> getProductsBySearch( + public CompletableFuture>>> getProductsBySearch( @RequestParam AreaCode areaCode, @RequestParam String keyword, @RequestParam LocalDate checkInDate, @@ -57,7 +57,7 @@ public CompletableFuture> searchProductFuture = CompletableFuture.supplyAsync(() -> + CompletableFuture> searchProductFuture = CompletableFuture.supplyAsync(() -> productService.getProductsBySearch(areaCode, keyword, checkInDate, checkOutDate, priceRange, cursorCheckInDate, cursorId, pageable, principalDetails) ); @@ -74,7 +74,7 @@ public CompletableFuture>>> getProductsByAreaCode( + public CompletableFuture>>> getProductsByAreaCode( @RequestParam AreaCode areaCode, @RequestParam(required = false) LocalDate cursorCheckInDate, @RequestParam(required = false) Long cursorId, @@ -86,7 +86,7 @@ public CompletableFuture> regionProductFuture = CompletableFuture.supplyAsync(() -> + CompletableFuture> regionProductFuture = CompletableFuture.supplyAsync(() -> productService.getProductsByAreaCode(areaCode, cursorCheckInDate, cursorId, pageable, principalDetails) ); diff --git a/src/main/java/site/goldenticket/domain/product/dto/RegionProductResponse.java b/src/main/java/site/goldenticket/domain/product/dto/RegionProductResponse.java index 29aa57e7..9682d1e8 100644 --- a/src/main/java/site/goldenticket/domain/product/dto/RegionProductResponse.java +++ b/src/main/java/site/goldenticket/domain/product/dto/RegionProductResponse.java @@ -1,16 +1,15 @@ package site.goldenticket.domain.product.dto; -import org.springframework.data.domain.Slice; import site.goldenticket.domain.product.model.Product; +import site.goldenticket.domain.product.repository.CustomSlice; import java.util.List; import java.util.stream.Collectors; public record RegionProductResponse( - long totalCount, List wishedProductResponseList ) { - public static RegionProductResponse fromEntity(long totalCount, Slice productSlice, boolean isAuthenticated) { + public static RegionProductResponse fromEntity(CustomSlice productSlice, boolean isAuthenticated) { List wishedProductResponseList = productSlice.getContent().stream() .map( @@ -19,7 +18,6 @@ public static RegionProductResponse fromEntity(long totalCount, Slice p .collect(Collectors.toList()); return new RegionProductResponse( - totalCount, wishedProductResponseList ); } diff --git a/src/main/java/site/goldenticket/domain/product/dto/SearchProductResponse.java b/src/main/java/site/goldenticket/domain/product/dto/SearchProductResponse.java index ce9051ea..a1264751 100644 --- a/src/main/java/site/goldenticket/domain/product/dto/SearchProductResponse.java +++ b/src/main/java/site/goldenticket/domain/product/dto/SearchProductResponse.java @@ -1,9 +1,9 @@ package site.goldenticket.domain.product.dto; -import org.springframework.data.domain.Slice; import site.goldenticket.domain.product.constants.AreaCode; import site.goldenticket.domain.product.constants.PriceRange; import site.goldenticket.domain.product.model.Product; +import site.goldenticket.domain.product.repository.CustomSlice; import java.time.LocalDate; import java.util.List; @@ -15,13 +15,12 @@ public record SearchProductResponse( LocalDate checkInDate, LocalDate checkOutDate, String priceRange, - long totalCount, List wishedProductResponseList ) { public static SearchProductResponse fromEntity( AreaCode areaCode, String keyword, LocalDate checkInDate, LocalDate checkOutDate, - PriceRange priceRange, long totalCount, Slice productSlice, boolean isAuthenticated) { + PriceRange priceRange, CustomSlice productSlice, boolean isAuthenticated) { List wishedProductResponseList = productSlice.getContent().stream() .map( @@ -35,7 +34,6 @@ public static SearchProductResponse fromEntity( checkInDate, checkOutDate, priceRange.getLabel(), - totalCount, wishedProductResponseList ); } diff --git a/src/main/java/site/goldenticket/domain/product/repository/CustomSlice.java b/src/main/java/site/goldenticket/domain/product/repository/CustomSlice.java index 755b1ca1..29184207 100644 --- a/src/main/java/site/goldenticket/domain/product/repository/CustomSlice.java +++ b/src/main/java/site/goldenticket/domain/product/repository/CustomSlice.java @@ -1,97 +1,35 @@ package site.goldenticket.domain.product.repository; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.Sort; - -import java.util.Iterator; import java.util.List; -import java.util.function.Function; -public class CustomSlice implements Slice { +public class CustomSlice { private final List content; - private final Pageable pageable; private final boolean hasNext; private final long totalCount; - public CustomSlice(List content, Pageable pageable, boolean hasNext, long totalCount) { + public CustomSlice(List content, boolean hasNext, long totalCount) { this.content = content; - this.pageable = pageable; this.hasNext = hasNext; this.totalCount = totalCount; } - @Override - public int getNumber() { - return pageable.getPageNumber(); - } - - @Override - public int getSize() { - return pageable.getPageSize(); - } - - @Override - public int getNumberOfElements() { - return content.size(); - } - - @Override public List getContent() { return content; } - @Override public boolean hasContent() { return !content.isEmpty(); } - @Override - public Sort getSort() { - return pageable.getSort(); - } - - @Override - public boolean isFirst() { - return !hasPrevious(); - } - - @Override public boolean isLast() { return !hasNext; } - @Override public boolean hasNext() { return hasNext; } - @Override - public boolean hasPrevious() { - return pageable.getPageNumber() > 0; - } - - @Override - public Pageable nextPageable() { - return hasNext() ? pageable.next() : Pageable.unpaged(); - } - - @Override - public Pageable previousPageable() { - return hasPrevious() ? pageable.previousOrFirst() : Pageable.unpaged(); - } - - @Override - public Slice map(Function converter) { - return null; - } - - @Override - public Iterator iterator() { - return content.iterator(); - } - public long getTotalElements() { return totalCount; } diff --git a/src/main/java/site/goldenticket/domain/product/repository/ProductRepositoryImpl.java b/src/main/java/site/goldenticket/domain/product/repository/ProductRepositoryImpl.java index 86f3bdb6..f8898e67 100644 --- a/src/main/java/site/goldenticket/domain/product/repository/ProductRepositoryImpl.java +++ b/src/main/java/site/goldenticket/domain/product/repository/ProductRepositoryImpl.java @@ -83,7 +83,7 @@ public CustomSlice getProductsBySearch( content.remove(pageable.getPageSize()); } - return new CustomSlice<>(content, pageable, hasNext, totalCount); + return new CustomSlice<>(content, hasNext, totalCount); } @Override @@ -132,7 +132,7 @@ public CustomSlice getProductsByAreaCode( content.remove(pageable.getPageSize()); } - return new CustomSlice<>(content, pageable, hasNext, totalCount); + return new CustomSlice<>(content, hasNext, totalCount); } private BooleanExpression buildRegionCondition(QProduct product, AreaCode areaCode) { diff --git a/src/main/java/site/goldenticket/domain/product/service/ProductService.java b/src/main/java/site/goldenticket/domain/product/service/ProductService.java index cdf7648e..b10061bd 100644 --- a/src/main/java/site/goldenticket/domain/product/service/ProductService.java +++ b/src/main/java/site/goldenticket/domain/product/service/ProductService.java @@ -7,8 +7,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -49,13 +47,13 @@ public class ProductService { private final RestTemplateService restTemplateService; private final RedisService redisService; - private final ProductRepository productRepository; private final AlertService alertService; + private final ProductRepository productRepository; private final ApiUrlProperties properties; // 1. 키워드 검색 및 지역 검색 메서드 @Transactional(readOnly = true) - public Slice getProductsBySearch( + public CustomSlice getProductsBySearch( AreaCode areaCode, String keyword, LocalDate checkInDate, LocalDate checkOutDate, PriceRange priceRange, LocalDate cursorCheckInDate, Long cursorId, Pageable pageable, @@ -66,24 +64,22 @@ public Slice getProductsBySearch( boolean isAuthenticated = (userId != null); CustomSlice productSlice = productRepository.getProductsBySearch( - areaCode, keyword, checkInDate, checkOutDate, priceRange, cursorCheckInDate, cursorId, - pageable, userId + areaCode, keyword, checkInDate, checkOutDate, priceRange, cursorCheckInDate, cursorId, pageable, userId ); SearchProductResponse searchProductResponse = SearchProductResponse.fromEntity( - areaCode, keyword, checkInDate, checkOutDate, priceRange, - productSlice.getTotalElements(), productSlice, isAuthenticated + areaCode, keyword, checkInDate, checkOutDate, priceRange, productSlice, isAuthenticated ); - return new SliceImpl<>( - Collections.singletonList(searchProductResponse), - pageable, - productSlice.hasNext() + return new CustomSlice<>( + Collections.singletonList(searchProductResponse), + productSlice.hasNext(), + productSlice.getTotalElements() ); } @Transactional(readOnly = true) - public Slice getProductsByAreaCode( + public CustomSlice getProductsByAreaCode( AreaCode areaCode, LocalDate cursorCheckInDate, Long cursorId, Pageable pageable, PrincipalDetails principalDetails ) { @@ -96,13 +92,13 @@ public Slice getProductsByAreaCode( ); RegionProductResponse regionProductResponse = RegionProductResponse.fromEntity( - productSlice.getTotalElements(), productSlice, isAuthenticated + productSlice, isAuthenticated ); - return new SliceImpl<>( - Collections.singletonList(regionProductResponse), - pageable, - productSlice.hasNext() + return new CustomSlice<>( + Collections.singletonList(regionProductResponse), + productSlice.hasNext(), + productSlice.getTotalElements() ); } diff --git a/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java b/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java index 447962e0..0a43bba7 100644 --- a/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java +++ b/src/test/java/site/goldenticket/domain/product/controller/ProductControllerTest.java @@ -93,29 +93,6 @@ void getProductsBySearch() { fieldWithPath("status").type(STRING).description("응답 상태"), fieldWithPath("message").type(STRING).description("응답 메시지"), fieldWithPath("data.content").type(LIST).description("컨텐츠 리스트"), - fieldWithPath("data.pageable").type(OBJECT).description("페이징 정보"), - fieldWithPath("data.first").type(BOOLEAN).description("첫 번째 페이지 여부"), - fieldWithPath("data.last").type(BOOLEAN).description("마지막 페이지 여부"), - fieldWithPath("data.size").type(NUMBER).description("페이지 크기"), - fieldWithPath("data.number").type(NUMBER).description("페이지 번호"), - fieldWithPath("data.numberOfElements").type(NUMBER).description("현재 페이지의 요소 수"), - fieldWithPath("data.empty").type(BOOLEAN).description("데이터가 비어 있는지 여부"), - - // pageable - fieldWithPath("data.pageable.pageNumber").type(NUMBER).description("페이지 번호"), - fieldWithPath("data.pageable.pageSize").type(NUMBER).description("페이지 크기"), - fieldWithPath("data.pageable.sort").type(OBJECT).description("정렬 정보"), - fieldWithPath("data.pageable.sort.empty").type(BOOLEAN).description("정렬 정보가 비어 있는지 여부"), - fieldWithPath("data.pageable.sort.sorted").type(BOOLEAN).description("정렬 정보가 정렬되어 있는지 여부"), - fieldWithPath("data.pageable.sort.unsorted").type(BOOLEAN).description("정렬 정보가 정렬되어 있지 않은지 여부"), - fieldWithPath("data.pageable.offset").type(NUMBER).description("오프셋"), - fieldWithPath("data.pageable.paged").type(BOOLEAN).description("페이징 여부"), - fieldWithPath("data.pageable.unpaged").type(BOOLEAN).description("페이징되지 않은 경우 여부"), - - // sort fields - fieldWithPath("data.sort.empty").type(BOOLEAN).description("정렬 정보가 비어 있는지 여부"), - fieldWithPath("data.sort.sorted").type(BOOLEAN).description("정렬 정보가 정렬되어 있는지 여부"), - fieldWithPath("data.sort.unsorted").type(BOOLEAN).description("정렬 정보가 정렬되어 있지 않은지 여부"), // content fields fieldWithPath("data.content[0].areaName").type(STRING).description("지역명"), @@ -123,7 +100,6 @@ void getProductsBySearch() { fieldWithPath("data.content[0].checkInDate").type(STRING).description("체크인 날짜"), fieldWithPath("data.content[0].checkOutDate").type(STRING).description("체크아웃 날짜"), fieldWithPath("data.content[0].priceRange").type(STRING).description("가격대"), - fieldWithPath("data.content[0].totalCount").type(NUMBER).description("총 개수"), // productResponseList fieldWithPath("data.content[0].wishedProductResponseList").type(LIST).description("상품 응답 리스트"), @@ -142,7 +118,10 @@ void getProductsBySearch() { fieldWithPath("data.content[0].wishedProductResponseList[0].originPriceRatio").type(NUMBER).description("구매가 대비 할인율"), fieldWithPath("data.content[0].wishedProductResponseList[0].marketPriceRatio").type(NUMBER).description("야놀자 판매가 대비 할인율"), fieldWithPath("data.content[0].wishedProductResponseList[0].productStatus").type(STRING).description("상품 상태"), - fieldWithPath("data.content[0].wishedProductResponseList[0].isWished").type(BOOLEAN).description("찜 여부") + fieldWithPath("data.content[0].wishedProductResponseList[0].isWished").type(BOOLEAN).description("찜 여부"), + + fieldWithPath("data.totalElements").type(NUMBER).description("총 요소 수"), + fieldWithPath("data.last").type(BOOLEAN).description("마지막 페이지 여부") ) )) .when()