diff --git a/src/docs/asciidoc/product.adoc b/src/docs/asciidoc/product.adoc index 80e6bf5c..922658cf 100644 --- a/src/docs/asciidoc/product.adoc +++ b/src/docs/asciidoc/product.adoc @@ -1,5 +1,50 @@ +== 키워드 검색 + +=== 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[] + +=== 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 path parameters +include::{snippets}/products-details/path-parameters.adoc[] + === HTTP request include::{snippets}/products-details/http-request.adoc[] @@ -11,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[] @@ -25,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[] @@ -45,6 +96,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 @@ -56,9 +118,26 @@ include::{snippets}/products/history/completed-all/response-fields.adoc[] === HTTP response 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[] + +=== HTTP response fields +include::{snippets}/products/history/completed-details/response-fields.adoc[] + +=== HTTP response +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/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/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 db9ba3ba..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) { @@ -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/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 36cd2744..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; @@ -36,7 +34,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,15 +42,18 @@ @RequiredArgsConstructor public class ProductService { - private final RestTemplateService restTemplateService; + 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 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, @@ -64,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 ) { @@ -94,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() ); } @@ -202,7 +200,7 @@ public Product save(Product product) { private String buildReservationUrl(String endpoint, Long pathVariable) { return UriComponentsBuilder - .fromUriString(DISTRIBUTE_BASE_URL) + .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/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/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/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 3fe6a9ef..0a43bba7 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,12 +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.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +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; @@ -37,27 +38,209 @@ 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 private ProductRepository productRepository; - @Autowired private ChatRoomRepository chatRoomRepository; - @Autowired private ChatRepository chatRepository; - @Autowired private NegoRepository negoRepository; - @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("컨텐츠 리스트"), + + // 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("가격대"), + + // 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("찜 여부"), + + fieldWithPath("data.totalElements").type(NUMBER).description("총 요소 수"), + fieldWithPath("data.last").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 + .given(spec).log().all() + .header("Authorization", "Bearer " + accessToken) + .filter(document( + "reservations-all", + getDocumentResponse(), + pathParameters( + parameterWithName("yaUserId").description("야놀자 ID") + ), + 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, yaUserId) + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + @Test + @DisplayName("상품 등록") + void registerProduct() { + // given + ProductRequest request = createProductRequest(); + + String url = "/products/{reservationId}"; + Long reservationId = 1L; + + // when + ExtractableResponse response = RestAssured + .given(spec).log().all() + .contentType(APPLICATION_JSON_VALUE) + .body(request) + .header("Authorization", "Bearer " + accessToken) + .filter(document( + "product-create", + getDocumentRequest(), + getDocumentResponse(), + pathParameters( + parameterWithName("reservationId").description("예약 ID") + ), + 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, reservationId) + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + @Test @DisplayName("상품 상세 조회") void getProduct() { @@ -65,13 +248,11 @@ void getProduct() { Product product = saveProduct(); String url = "/products/{productId}"; - String pathName = "productId"; - Long pathValues = product.getId(); + Long productId = product.getId(); // when ExtractableResponse response = RestAssured .given(spec).log().all() - .pathParam(pathName, pathValues) .filter(document( "products-details", getDocumentResponse(), @@ -79,62 +260,36 @@ void getProduct() { 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, productId) .then().log().all() .extract(); @@ -146,18 +301,15 @@ 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(); + Long productId = product.getId(); // when ExtractableResponse response = RestAssured .given(spec).log().all() - .pathParam(pathName, pathValues) .contentType(APPLICATION_JSON_VALUE) .body(request) .header("Authorization", "Bearer " + accessToken) @@ -169,22 +321,17 @@ void updateProduct() { 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, productId) .then().log().all() .extract(); @@ -199,13 +346,11 @@ void deleteProduct() { Product product = saveProduct(); String url = "/products/{productId}"; - String pathName = "productId"; - Long pathValues = product.getId(); + Long productId = product.getId(); // when ExtractableResponse response = RestAssured .given(spec).log().all() - .pathParam(pathName, pathValues) .header("Authorization", "Bearer " + accessToken) .filter(document( "products-delete", @@ -214,16 +359,13 @@ void deleteProduct() { 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, productId) .then().log().all() .extract(); @@ -250,56 +392,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 +444,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 +467,7 @@ void getAllCompletedProducts() { } @Test + @DisplayName("판매 내역 - 판매 완료 상세 조회") void getCompletedProductDetails() { // given Product product = saveSoldOutProduct(); @@ -368,17 +475,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 @@ -388,13 +534,11 @@ void deleteCompletedProduct(){ Product product = saveSoldOutProduct(); String url = "/products/history/completed/{productId}"; - String pathName = "productId"; - Long pathValues = product.getId(); + Long productId = product.getId(); // when ExtractableResponse response = RestAssured .given(spec).log().all() - .pathParam(pathName, pathValues) .header("Authorization", "Bearer " + accessToken) .filter(document( "products/history/completed-delete", @@ -403,16 +547,13 @@ void deleteCompletedProduct(){ 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, productId) .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/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 00000000..87d38dfd --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,38 @@ +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 + #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