diff --git a/README.md b/README.md index 42b1f59e0..b020d8438 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,49 @@ * 토큰 받기를 읽고 액세스 토큰을 추출한다. * 앱 키, 인가 코드가 절대 유출되지 않도록 한다. * 특히 시크릿 키는 GitHub나 클라이언트 코드 등 외부에서 볼 수 있는 곳에 추가하지 않는다. -* (선택) 인가 코드를 받는 방법이 불편한 경우 카카오 로그인 화면을 구현한다. \ No newline at end of file +* (선택) 인가 코드를 받는 방법이 불편한 경우 카카오 로그인 화면을 구현한다. + +### 🚀 2단계 - 주문하기 +*** +#### 기능 요구 사항 +카카오톡 메시지 API를 사용하여 주문하기 기능을 구현한다. + +* 주문할 때 수령인에게 보낼 메시지를 작성할 수 있다. +* 상품 옵션과 해당 수량을 선택하여 주문하면 해당 상품 옵션의 수량이 차감된다. +* 해당 상품이 위시 리스트에 있는 경우 위시 리스트에서 삭제한다. +* 나에게 보내기를 읽고 주문 내역을 카카오톡 메시지로 전송한다. + * 메시지는 메시지 템플릿의 기본 템플릿이나 사용자 정의 템플릿을 사용하여 자유롭게 작성한다. + +아래 예시와 같이 HTTP 메시지를 주고받도록 구현한다. + +##### Request +``` +POST /api/orders HTTP/1.1 +Authorization: Bearer {token} +Content-Type: application/json + +{ + "optionId": 1, + "quantity": 2, + "message": "Please handle this order with care." +} +``` + +##### Response +``` +HTTP/1.1 201 Created +Content-Type: application/json + +{ +"id": 1, +"optionId": 1, +"quantity": 2, +"orderDateTime": "2024-07-21T10:00:00", +"message": "Please handle this order with care." +} +``` +### 🚀 3단계 - API 문서 만들기 +*** +#### 기능 요구 사항 +API 사양에 관해 클라이언트와 어떻게 소통할 수 있을까? +어떻게 하면 편하게 소통할 수 있을지 고민해 보고 그 방법을 구현한다. \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3b37a3492..c3ebf4db9 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.3.1' id 'io.spring.dependency-management' version '1.1.5' + id 'org.asciidoctor.jvm.convert' version '4.0.3' } group = 'camp.nextstep.edu' @@ -17,6 +18,13 @@ repositories { mavenCentral() } +configurations { + asciidoctorExt //asciidoctorExt를 configurations로 지정 + compileOnly { + extendsFrom annotationProcessor + } +} + dependencies { implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' @@ -31,6 +39,13 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + // Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4' + + //REST Docs + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' @@ -45,3 +60,36 @@ dependencyManagement { tasks.named('test') { useJUnitPlatform() } + +ext { + set('snippetsDir', file("build/generated-snippets")) //snippets 파일이 저장될 경로 snippetsDir로 변수 설정 +} + +test { + outputs.dir snippetsDir + useJUnitPlatform() +} + +asciidoctor { + configurations 'asciidoctorExt' //Asciidoctor에서 asciidoctorExt를 사용하도록 설정 + baseDirFollowsSourceFile() + inputs.dir snippetsDir + dependsOn test //gradle build 시 test -> asciiDoctor 순서로 실행 +} + +asciidoctor.doFirst { + delete file('src/main/resources/static/docs') //asciidoctor 실행 전 docs 폴더 삭제 +} + +task createDocument(type: Copy) { + dependsOn asciidoctor + from file("build/docs/asciidoc") + into file("src/main/resources/static") +} + +bootJar { + dependsOn createDocument //Gradle build 시, createDocument -> bootJar 순으로 실행 + from("${asciidoctor.outputDir}") { + into 'static/docs' + } +} \ No newline at end of file diff --git a/src/docs/asciidoc/category.adoc b/src/docs/asciidoc/category.adoc new file mode 100644 index 000000000..76b342098 --- /dev/null +++ b/src/docs/asciidoc/category.adoc @@ -0,0 +1,19 @@ +== 카테고리 API + +=== 카테고리 생성 +operation::category-api-controller-test/create-category[] + +=== 카테고리 수정 +operation::category-api-controller-test/update-category[] + +=== 카테고리 삭제 +operation::category-api-controller-test/delete-category[] + +=== 단일 카테고리 조회 +operation::category-api-controller-test/read-category[] + +=== 전체 카테고리 조회 +operation::category-api-controller-test/read-all-categories[] + + + diff --git a/src/docs/asciidoc/docs.adoc b/src/docs/asciidoc/docs.adoc new file mode 100644 index 000000000..5ebd45ef9 --- /dev/null +++ b/src/docs/asciidoc/docs.adoc @@ -0,0 +1,11 @@ += 카카오 테크 캠퍼스 STEP 2 - REST Docs +:doctype: book +:source-highlighter: highlightjs +:toc: left +:toclevels: 3 +:sectlinks: + +include::product.adoc[] +include::category.adoc[] +include::member.adoc[] +include::login.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/login.adoc b/src/docs/asciidoc/login.adoc new file mode 100644 index 000000000..a31559600 --- /dev/null +++ b/src/docs/asciidoc/login.adoc @@ -0,0 +1,4 @@ +== 소셜 로그인 API + +=== 카카오 로그인 +operation::login-controller-test/kakao-login[] \ No newline at end of file diff --git a/src/docs/asciidoc/member.adoc b/src/docs/asciidoc/member.adoc new file mode 100644 index 000000000..5676d7a93 --- /dev/null +++ b/src/docs/asciidoc/member.adoc @@ -0,0 +1,19 @@ +== 회원 API + +=== 회원가입 +operation::member-api-controller-test/create-member[] + +=== 로그인 +operation::member-api-controller-test/login[] + +=== 회원 조회 +operation::member-api-controller-test/read-member[] + +=== 위시 상품 조회 +operation::member-api-controller-test/read-wish-product[] + +=== 위시 상품 수정 +operation::member-api-controller-test/update-wish-product[] + +=== 위시 상품 삭제 +operation::member-api-controller-test/delete-wish-product[] diff --git a/src/docs/asciidoc/product.adoc b/src/docs/asciidoc/product.adoc new file mode 100644 index 000000000..65987e449 --- /dev/null +++ b/src/docs/asciidoc/product.adoc @@ -0,0 +1,35 @@ +== 상품 API + +=== 전체 상품 조회 +operation::product-api-controller-test/read-all-products[] + +=== 단일 상품 조회 +operation::product-api-controller-test/read-product[] + +=== 상품 생성 +operation::product-api-controller-test/create-product[] + +=== 상품 수정 +operation::product-api-controller-test/update-product[] + +=== 상품 삭제 +operation::product-api-controller-test/delete-product[] + +=== 상품 주문 +operation::product-api-controller-test/order-product[] + +=== 위시 상품 추가 +operation::product-api-controller-test/create-wish-product[] + +=== 상품 옵션 생성 +operation::product-api-controller-test/create-option[] + +=== 상품 옵션 조회 +operation::product-api-controller-test/read-options[] + +=== 상품 옵션 수정 +operation::product-api-controller-test/update-option[] + +=== 상품 옵션 삭제 +operation::product-api-controller-test/delete-option[] + diff --git a/src/main/java/gift/authentication/token/Token.java b/src/main/java/gift/authentication/token/Token.java index 8ff73f026..ba75ba991 100644 --- a/src/main/java/gift/authentication/token/Token.java +++ b/src/main/java/gift/authentication/token/Token.java @@ -15,6 +15,10 @@ public static Token from(String value) { return new Token(value); } + public static Token fromBearer(String value) { + return new Token(value.replace("Bearer ", "")); + } + public String getValue() { return value; } diff --git a/src/main/java/gift/config/KakaoProperties.java b/src/main/java/gift/config/KakaoProperties.java index baa7b457c..04355b18b 100644 --- a/src/main/java/gift/config/KakaoProperties.java +++ b/src/main/java/gift/config/KakaoProperties.java @@ -1,5 +1,6 @@ package gift.config; +import java.net.URI; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.bind.ConstructorBinding; @@ -13,10 +14,12 @@ public class KakaoProperties { private final String userInfoUrl; private final String tokenUrl; private final String responseType; + private final String messageUrl; @ConstructorBinding public KakaoProperties(String clientId, String redirectUri, String contentType, - String grantType, String userInfoUrl, String tokenUrl, String responseType) { + String grantType, String userInfoUrl, String tokenUrl, String responseType, + String messageUrl) { this.clientId = clientId; this.redirectUri = redirectUri; this.contentType = contentType; @@ -24,6 +27,7 @@ public KakaoProperties(String clientId, String redirectUri, String contentType, this.userInfoUrl = userInfoUrl; this.tokenUrl = tokenUrl; this.responseType = responseType; + this.messageUrl = messageUrl; } public String getClientId() { @@ -53,4 +57,20 @@ public String getTokenUrl() { public String getResponseType() { return responseType; } + + public String getMessageUrl() { + return messageUrl; + } + + public URI getUserInfoUrlAsUri() { + return URI.create(userInfoUrl); + } + + public URI getTokenUrlAsUri() { + return URI.create(tokenUrl); + } + + public URI getMessageUrlAsUri() { + return URI.create(messageUrl); + } } diff --git a/src/main/java/gift/domain/Order.java b/src/main/java/gift/domain/Order.java new file mode 100644 index 000000000..6595048f2 --- /dev/null +++ b/src/main/java/gift/domain/Order.java @@ -0,0 +1,132 @@ +package gift.domain; + +import gift.domain.base.BaseEntity; +import gift.domain.base.BaseTimeEntity; +import gift.domain.constants.OrderStatus; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.DynamicInsert; + +@Entity +@DynamicInsert +@Table(name = "orders") +public class Order extends BaseEntity { + + @Column(nullable = false) + private Long memberId; + + @Column(nullable = false) + private Long productId; + + @Column(nullable = false) + private Long productOptionId; + + @Column(nullable = false) + private Integer quantity; + + @Column(nullable = false) + @ColumnDefault("''") + private String message; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + @ColumnDefault("'ORDERED'") + private OrderStatus orderStatus; + + protected Order() {} + + public static class Builder extends BaseTimeEntity.Builder { + + private Long memberId; + + private Long productId; + + private Long productOptionId; + + private Integer quantity; + + private String message; + + public Builder memberId(Long memberId) { + this.memberId = memberId; + return this; + } + + public Builder productId(Long productId) { + this.productId = productId; + return this; + } + + public Builder productOptionId(Long productOptionId) { + this.productOptionId = productOptionId; + return this; + } + + public Builder quantity(Integer quantity) { + this.quantity = quantity; + return this; + } + + public Builder message(String message) { + this.message = message; + return this; + } + + @Override + protected Builder self() { + return this; + } + + @Override + public Order build() { + return new Order(this); + } + } + + public Order(Builder builder) { + super(builder); + this.memberId = builder.memberId; + this.productId = builder.productId; + this.productOptionId = builder.productOptionId; + this.quantity = builder.quantity; + this.message = builder.message; + } + + public Long getMemberId() { + return memberId; + } + + public Long getProductId() { + return productId; + } + + public Long getProductOptionId() { + return productOptionId; + } + + public Integer getQuantity() { + return quantity; + } + + public String getMessage() { + return message; + } + + public OrderStatus getOrderStatus() { + return orderStatus; + } + + public Order complete() { + this.orderStatus = OrderStatus.COMPLETED; + return this; + } + + public Order cancel() { + this.orderStatus = OrderStatus.CANCELED; + return this; + } +} diff --git a/src/main/java/gift/domain/constants/OrderStatus.java b/src/main/java/gift/domain/constants/OrderStatus.java new file mode 100644 index 000000000..069537257 --- /dev/null +++ b/src/main/java/gift/domain/constants/OrderStatus.java @@ -0,0 +1,9 @@ +package gift.domain.constants; + +public enum OrderStatus { + + ORDERED, //주문됨 + CANCELED, //취소됨 + COMPLETED //배송완료 + +} diff --git a/src/main/java/gift/repository/OrderRepository.java b/src/main/java/gift/repository/OrderRepository.java new file mode 100644 index 000000000..2cd02aa2b --- /dev/null +++ b/src/main/java/gift/repository/OrderRepository.java @@ -0,0 +1,10 @@ +package gift.repository; + +import gift.domain.Order; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface OrderRepository extends JpaRepository { + +} diff --git a/src/main/java/gift/service/LoginService.java b/src/main/java/gift/service/LoginService.java index 03ec44394..dc78e70f1 100644 --- a/src/main/java/gift/service/LoginService.java +++ b/src/main/java/gift/service/LoginService.java @@ -9,9 +9,6 @@ import gift.web.client.dto.KakaoAccount; import gift.web.client.dto.KakaoInfo; import gift.web.dto.response.LoginResponse; -import gift.web.validation.exception.client.InvalidCredentialsException; -import java.net.URI; -import java.net.URISyntaxException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -47,30 +44,22 @@ public LoginResponse kakaoLogin(final String authorizationCode){ return new LoginResponse(accessToken.getValue()); } - private KakaoToken getToken(String authorizationCode) { - try { - return kakaoClient.getToken( - new URI(kakaoProperties.getTokenUrl()), - authorizationCode, - kakaoProperties.getClientId(), - kakaoProperties.getRedirectUri(), - kakaoProperties.getGrantType()); - } catch (URISyntaxException e) { - throw new InvalidCredentialsException(e); - } + private KakaoToken getToken(final String authorizationCode) { + return kakaoClient.getToken( + kakaoProperties.getTokenUrlAsUri(), + authorizationCode, + kakaoProperties.getClientId(), + kakaoProperties.getRedirectUri(), + kakaoProperties.getGrantType()); } - private KakaoInfo getInfo(KakaoToken kakaoToken) { - try { - return kakaoClient.getKakaoInfo( - new URI(kakaoProperties.getUserInfoUrl()), - getBearerToken(kakaoToken)); - } catch (URISyntaxException e) { - throw new InvalidCredentialsException(e); - } + private KakaoInfo getInfo(final KakaoToken kakaoToken) { + return kakaoClient.getKakaoInfo( + kakaoProperties.getUserInfoUrlAsUri(), + getBearerToken(kakaoToken)); } - private String getBearerToken(KakaoToken kakaoToken) { + private String getBearerToken(final KakaoToken kakaoToken) { return kakaoToken.getTokenType() + " " + kakaoToken.getAccessToken(); } } diff --git a/src/main/java/gift/service/OrderService.java b/src/main/java/gift/service/OrderService.java new file mode 100644 index 000000000..3534ae8b6 --- /dev/null +++ b/src/main/java/gift/service/OrderService.java @@ -0,0 +1,84 @@ +package gift.service; + +import gift.authentication.token.JwtResolver; +import gift.authentication.token.Token; +import gift.config.KakaoProperties; +import gift.domain.Order; +import gift.repository.OrderRepository; +import gift.web.client.KakaoClient; +import gift.web.client.dto.KakaoCommerce; +import gift.web.dto.request.order.CreateOrderRequest; +import gift.web.dto.response.order.OrderResponse; +import gift.web.dto.response.product.ReadProductResponse; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class OrderService { + + private final KakaoClient kakaoClient; + private final KakaoProperties kakaoProperties; + + private final JwtResolver jwtResolver; + + private final ProductOptionService productOptionService; + private final ProductService productService; + private final OrderRepository orderRepository; + + public OrderService(KakaoClient kakaoClient, JwtResolver jwtResolver, ProductOptionService productOptionService, + ProductService productService, KakaoProperties kakaoProperties, + OrderRepository orderRepository) { + this.kakaoClient = kakaoClient; + this.jwtResolver = jwtResolver; + this.productOptionService = productOptionService; + this.productService = productService; + this.kakaoProperties = kakaoProperties; + this.orderRepository = orderRepository; + } + + /** + * 주문을 생성합니다
+ * 카카오 로그인을 통해 서비스를 이용 중인 회원은 나에게 보내기를 통해 알림을 전송합니다. + * @param accessToken 우리 서비스의 토큰 + * @param productId 구매할 상품 ID + * @param memberId 구매자 ID + * @param request 주문 요청 + * @return + */ + @Transactional + public OrderResponse createOrder(String accessToken, Long productId, Long memberId, CreateOrderRequest request) { + //상품 옵션 수량 차감 + productOptionService.subtractOptionStock(request); + + //주문 정보 저장 + Order order = orderRepository.save(request.toEntity(memberId, productId)); + + sendOrderMessageIfSocialMember(accessToken, productId, request); + return OrderResponse.from(order); + } + + /** + * 소셜 로그인을 통해 주문한 경우 카카오톡 메시지를 전송합니다 + * @param accessToken 우리 서비스의 토큰 + * @param productId 상품 ID + * @param request 주문 요청 + */ + private void sendOrderMessageIfSocialMember(String accessToken, Long productId, CreateOrderRequest request) { + jwtResolver.resolveSocialToken(Token.fromBearer(accessToken)) + .ifPresent(socialToken -> + kakaoClient.sendMessage( + kakaoProperties.getMessageUrlAsUri(), + getBearerToken(socialToken), + generateKakaoCommerce(productId, request).toJson() + )); + } + + private KakaoCommerce generateKakaoCommerce(Long productId, CreateOrderRequest request) { + ReadProductResponse productResponse = productService.readProductById(productId); + return KakaoCommerce.of(productResponse, request.getMessage()); + } + + private String getBearerToken(String token) { + return "Bearer " + token; + } +} diff --git a/src/main/java/gift/service/ProductOptionService.java b/src/main/java/gift/service/ProductOptionService.java index 9663b89a5..dc5f7650c 100644 --- a/src/main/java/gift/service/ProductOptionService.java +++ b/src/main/java/gift/service/ProductOptionService.java @@ -2,6 +2,8 @@ import gift.domain.ProductOption; import gift.repository.ProductOptionRepository; +import gift.repository.ProductRepository; +import gift.web.dto.request.order.CreateOrderRequest; import gift.web.dto.request.productoption.CreateProductOptionRequest; import gift.web.dto.request.productoption.SubtractProductOptionQuantityRequest; import gift.web.dto.request.productoption.UpdateProductOptionRequest; @@ -21,13 +23,17 @@ public class ProductOptionService { private final ProductOptionRepository productOptionRepository; + private final ProductRepository productRepository; - public ProductOptionService(ProductOptionRepository productOptionRepository) { + public ProductOptionService(ProductOptionRepository productOptionRepository, + ProductRepository productRepository) { this.productOptionRepository = productOptionRepository; + this.productRepository = productRepository; } @Transactional public CreateProductOptionResponse createOption(Long productId, CreateProductOptionRequest request) { + validateExistsProduct(productId); String optionName = request.getName(); validateOptionNameExists(productId, optionName); @@ -36,6 +42,15 @@ public CreateProductOptionResponse createOption(Long productId, CreateProductOpt return CreateProductOptionResponse.fromEntity(createdOption); } + /** + * 상품이 존재하는지 확인합니다. + * @param productId 상품 아이디 + */ + private void validateExistsProduct(Long productId) { + productRepository.findById(productId) + .orElseThrow(() -> new ResourceNotFoundException("상품 아이디: ", productId.toString())); + } + /** * 상품 옵션 이름이 이미 존재하는지 확인합니다.
* 이미 존재한다면 {@link AlreadyExistsException} 을 발생시킵니다. @@ -131,6 +146,10 @@ public SubtractProductOptionQuantityResponse subtractOptionStock(Long optionId, return SubtractProductOptionQuantityResponse.fromEntity(option); } + public SubtractProductOptionQuantityResponse subtractOptionStock(CreateOrderRequest request) { + return subtractOptionStock(request.getOptionId(), new SubtractProductOptionQuantityRequest(request.getQuantity())); + } + @Transactional public void deleteOption(Long optionId) { ProductOption option = productOptionRepository.findById(optionId) diff --git a/src/main/java/gift/web/client/KakaoClient.java b/src/main/java/gift/web/client/KakaoClient.java index 401e1c28e..af3986a9d 100644 --- a/src/main/java/gift/web/client/KakaoClient.java +++ b/src/main/java/gift/web/client/KakaoClient.java @@ -1,10 +1,15 @@ package gift.web.client; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + import gift.authentication.token.KakaoToken; import gift.web.client.dto.KakaoInfo; +import gift.web.client.dto.KakaoMessageResult; import java.net.URI; import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; @@ -14,7 +19,7 @@ public interface KakaoClient { @PostMapping KakaoInfo getKakaoInfo( URI uri, - @RequestHeader("Authorization") String accessToken); + @RequestHeader(AUTHORIZATION) String accessToken); @PostMapping KakaoToken getToken( @@ -23,4 +28,17 @@ KakaoToken getToken( @RequestParam("client_id") String clientId, @RequestParam("redirect_uri") String redirectUrl, @RequestParam("grant_type") String grantType); + + /** + * 카카오톡 메시지 - 나에게 보내기 + * @param uri {@link https://kapi.kakao.com/v2/api/talk/memo/default/send} 으로 고정 + * @param accessToken Bearer Token + * @param templateObject 메시지 구성 요소를 담은 객체(Object) 피드, 리스트, 위치, 커머스, 텍스트, 캘린더 중 하나 + */ + @PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + KakaoMessageResult sendMessage( + URI uri, + @RequestHeader(AUTHORIZATION) String accessToken, + @RequestBody String templateObject + ); } diff --git a/src/main/java/gift/web/client/dto/KakaoCommerce.java b/src/main/java/gift/web/client/dto/KakaoCommerce.java new file mode 100644 index 000000000..aec159aab --- /dev/null +++ b/src/main/java/gift/web/client/dto/KakaoCommerce.java @@ -0,0 +1,207 @@ +package gift.web.client.dto; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import gift.web.dto.response.product.ReadProductResponse; + +@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) +public class KakaoCommerce { + + private final String objectType = "commerce"; + private Content content; + private Commerce commerce; + + @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) + static class Content { + private String title; + private String imageUrl; + private Integer imageWidth; + private Integer imageHeight; + private String description; + private Link link; + + public Content(String title, String imageUrl, Integer imageWidth, Integer imageHeight, + String description, Link link) { + this.title = title; + this.imageUrl = imageUrl; + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + this.description = description; + this.link = link; + } + + @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) + static class Link { + private String webUrl; + private String mobileWebUrl; + private String androidExecutionParams; + private String iosExecutionParams; + + public Link(String webUrl, String mobileWebUrl, String androidExecutionParams, + String iosExecutionParams) { + this.webUrl = webUrl; + this.mobileWebUrl = mobileWebUrl; + this.androidExecutionParams = androidExecutionParams; + this.iosExecutionParams = iosExecutionParams; + } + + public String getWebUrl() { + return webUrl; + } + + public String getMobileWebUrl() { + return mobileWebUrl; + } + + public String getAndroidExecutionParams() { + return androidExecutionParams; + } + + public String getIosExecutionParams() { + return iosExecutionParams; + } + } + + public String getTitle() { + return title; + } + + public String getImageUrl() { + return imageUrl; + } + + public Integer getImageWidth() { + return imageWidth; + } + + public Integer getImageHeight() { + return imageHeight; + } + + public String getDescription() { + return description; + } + + public Link getLink() { + return link; + } + } + + @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) + static class Commerce { + private Integer regularPrice; + private Integer discountPrice; + private Integer discountRate; + + public Commerce(Integer regularPrice, Integer discountPrice, Integer discountRate) { + this.regularPrice = regularPrice; + this.discountPrice = discountPrice; + this.discountRate = discountRate; + } + + public Commerce(Integer regularPrice) { + this.regularPrice = regularPrice; + } + + public Integer getRegularPrice() { + return regularPrice; + } + + public Integer getDiscountPrice() { + return discountPrice; + } + + public Integer getDiscountRate() { + return discountRate; + } + } + + public KakaoCommerce() { + + } + + public KakaoCommerce setCommerce(Integer regularPrice, Integer discountPrice, Integer discountRate) { + this.commerce = new Commerce(regularPrice, discountPrice, discountRate); + return this; + } + + public KakaoCommerce setCommerce(Integer regularPrice) { + this.commerce = new Commerce(regularPrice); + return this; + } + + public KakaoCommerce setContent(String title, String imageUrl, Integer imageWidth, Integer imageHeight, String description, String webUrl, String mobileWebUrl, String androidExecutionParams, String iosExecutionParams) { + this.content = new Content(title, imageUrl, imageWidth, imageHeight, description, + new Content.Link(webUrl, mobileWebUrl, androidExecutionParams, iosExecutionParams)); + return this; + } + + public String getObjectType() { + return objectType; + } + + public Content getContent() { + return content; + } + + public Commerce getCommerce() { + return commerce; + } + + /** + * 카카오 상거래 메시지 생성 - 할인 적용 + * @param product 상품 정보 + * @param discountRate 할인율 (0 ~ 100) + * @param message 메시지 + * @return + */ + public static KakaoCommerce of(ReadProductResponse product, int discountRate, String message) { + return new KakaoCommerce().setContent( + product.getName(), + product.getImageUrl().toString(), + 640, + 640, + message, + "https://localhost:8080", + "https://localhost:8080", + "contentId=" + product.getId(), + "contentId=" + product.getId() + ).setCommerce(product.getPrice(), product.getPrice(), 0); + } + + /** + * 카카오 상거래 메시지 생성 - 할인 없음 + * @param product 상품 정보 + * @param message 메시지 + * @return + */ + public static KakaoCommerce of(ReadProductResponse product, String message) { + return new KakaoCommerce().setContent( + product.getName(), + product.getImageUrl().toString(), + 640, + 640, + message, + "https://localhost:8080", + "https://localhost:8080", + "contentId=" + product.getId(), + "contentId=" + product.getId() + ).setCommerce(product.getPrice()); + } + + public String toJson() { + ObjectMapper mapper = new ObjectMapper(); + try { + return "template_object=" + mapper.writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new RuntimeException("JSON 변환 실패", e); + } + } + + public String getContentDescription() { + return content.getDescription(); + } +} + diff --git a/src/main/java/gift/web/client/dto/KakaoMessageResult.java b/src/main/java/gift/web/client/dto/KakaoMessageResult.java new file mode 100644 index 000000000..5517133b0 --- /dev/null +++ b/src/main/java/gift/web/client/dto/KakaoMessageResult.java @@ -0,0 +1,17 @@ +package gift.web.client.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public class KakaoMessageResult { + + private final Integer resultCode; + + @JsonCreator + public KakaoMessageResult(Integer resultCode) { + this.resultCode = resultCode; + } + + public Integer getResultCode() { + return resultCode; + } +} diff --git a/src/main/java/gift/web/controller/api/ProductApiController.java b/src/main/java/gift/web/controller/api/ProductApiController.java index 2e2ec7181..45c4aad4e 100644 --- a/src/main/java/gift/web/controller/api/ProductApiController.java +++ b/src/main/java/gift/web/controller/api/ProductApiController.java @@ -1,15 +1,18 @@ package gift.web.controller.api; import gift.authentication.annotation.LoginMember; +import gift.service.OrderService; import gift.service.ProductOptionService; import gift.service.ProductService; import gift.service.WishProductService; import gift.web.dto.MemberDetails; +import gift.web.dto.request.order.CreateOrderRequest; import gift.web.dto.request.product.CreateProductRequest; import gift.web.dto.request.product.UpdateProductRequest; import gift.web.dto.request.productoption.CreateProductOptionRequest; import gift.web.dto.request.productoption.UpdateProductOptionRequest; import gift.web.dto.request.wishproduct.CreateWishProductRequest; +import gift.web.dto.response.order.OrderResponse; import gift.web.dto.response.product.CreateProductResponse; import gift.web.dto.response.product.ReadAllProductsResponse; import gift.web.dto.response.product.ReadProductResponse; @@ -23,6 +26,7 @@ import java.util.NoSuchElementException; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; @@ -31,6 +35,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -42,11 +47,14 @@ public class ProductApiController { private final ProductService productService; private final WishProductService wishProductService; private final ProductOptionService productOptionService; + private final OrderService orderService; - public ProductApiController(ProductService productService, WishProductService wishProductService, ProductOptionService productOptionService) { + public ProductApiController(ProductService productService, WishProductService wishProductService, ProductOptionService productOptionService, + OrderService orderService) { this.productService = productService; this.wishProductService = wishProductService; this.productOptionService = productOptionService; + this.orderService = orderService; } @GetMapping @@ -55,6 +63,12 @@ public ResponseEntity readAllProducts(@PageableDefault return ResponseEntity.ok(response); } + @GetMapping(params = "categoryId") + public ResponseEntity readProductsByCategoryId(@PageableDefault Pageable pageable, @RequestParam Long categoryId) { + ReadAllProductsResponse response = productService.readProductsByCategoryId(categoryId, pageable); + return ResponseEntity.ok(response); + } + @PostMapping public ResponseEntity createProduct( @Validated @RequestBody CreateProductRequest request) throws URISyntaxException { @@ -64,12 +78,6 @@ public ResponseEntity createProduct( return ResponseEntity.created(location).body(response); } - @GetMapping(params = "categoryId") - public ResponseEntity readProductsByCategoryId(@PageableDefault Pageable pageable, @RequestParam Long categoryId) { - ReadAllProductsResponse response = productService.readProductsByCategoryId(categoryId, pageable); - return ResponseEntity.ok(response); - } - @GetMapping("/{productId}") public ResponseEntity readProduct(@PathVariable Long productId) { ReadProductResponse response; @@ -114,6 +122,17 @@ public ResponseEntity createOption(@PathVariable Lo return ResponseEntity.created(location).body(response); } + @PostMapping("/{productId}/order") + public ResponseEntity orderProduct( + @RequestHeader(HttpHeaders.AUTHORIZATION) String accessToken, + @PathVariable Long productId, + @RequestBody @Validated CreateOrderRequest request, + @LoginMember MemberDetails memberDetails + ) { + OrderResponse response = orderService.createOrder(accessToken, productId, memberDetails.getId(), request); + return ResponseEntity.ok(response); + } + @PutMapping("/{productId}/options/{optionId}") public ResponseEntity updateOption(@PathVariable Long productId, @PathVariable Long optionId, @Validated @RequestBody UpdateProductOptionRequest request) { UpdateProductOptionResponse response = productOptionService.updateOption(optionId, productId, request); diff --git a/src/main/java/gift/web/controller/view/ProductViewController.java b/src/main/java/gift/web/controller/view/ProductViewController.java index d13e7df69..44aa5e0ae 100644 --- a/src/main/java/gift/web/controller/view/ProductViewController.java +++ b/src/main/java/gift/web/controller/view/ProductViewController.java @@ -1,6 +1,7 @@ package gift.web.controller.view; import gift.authentication.annotation.LoginMember; +import gift.config.KakaoProperties; import gift.service.ProductService; import gift.web.dto.MemberDetails; import gift.web.dto.form.CreateProductForm; @@ -19,8 +20,11 @@ public class ProductViewController { private final ProductService productService; - public ProductViewController(ProductService productService) { + private final KakaoProperties kakaoProperties; + + public ProductViewController(ProductService productService, KakaoProperties kakaoProperties) { this.productService = productService; + this.kakaoProperties = kakaoProperties; } @GetMapping("/products") @@ -45,7 +49,9 @@ public String editForm(@PathVariable Long id, Model model) { } @GetMapping("/login") - public String loginForm() { + public String loginForm(Model model) { + model.addAttribute("clientId", kakaoProperties.getClientId()); + model.addAttribute("redirectUri", kakaoProperties.getRedirectUri()); return "form/login-form"; } diff --git a/src/main/java/gift/web/dto/request/order/CreateOrderRequest.java b/src/main/java/gift/web/dto/request/order/CreateOrderRequest.java new file mode 100644 index 000000000..13e28791f --- /dev/null +++ b/src/main/java/gift/web/dto/request/order/CreateOrderRequest.java @@ -0,0 +1,46 @@ +package gift.web.dto.request.order; + +import gift.domain.Order; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +public class CreateOrderRequest { + + @NotNull + private final Long optionId; + + @Min(1) + private final Integer quantity; + + @NotEmpty + private final String message; + + public CreateOrderRequest(Long optionId, Integer quantity, String message) { + this.optionId = optionId; + this.quantity = quantity; + this.message = message; + } + + public Order toEntity(Long memberId, Long productId) { + return new Order.Builder() + .memberId(memberId) + .productId(productId) + .productOptionId(optionId) + .quantity(quantity) + .message(message) + .build(); + } + + public Long getOptionId() { + return optionId; + } + + public Integer getQuantity() { + return quantity; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/gift/web/dto/response/order/OrderResponse.java b/src/main/java/gift/web/dto/response/order/OrderResponse.java new file mode 100644 index 000000000..6601555e4 --- /dev/null +++ b/src/main/java/gift/web/dto/response/order/OrderResponse.java @@ -0,0 +1,41 @@ +package gift.web.dto.response.order; + +import gift.domain.Order; + +public class OrderResponse { + + private Long productId; + + private Long optionId; + + private Integer quantity; + + private String message; + + public OrderResponse(Long productId, Long optionId, Integer quantity, String message) { + this.productId = productId; + this.optionId = optionId; + this.quantity = quantity; + this.message = message; + } + + public static OrderResponse from(Order order) { + return new OrderResponse(order.getProductId(), order.getProductOptionId(), order.getQuantity(), order.getMessage()); + } + + public Long getProductId() { + return productId; + } + + public Long getOptionId() { + return optionId; + } + + public Integer getQuantity() { + return quantity; + } + + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/src/main/java/gift/web/dto/response/product/ReadProductResponse.java b/src/main/java/gift/web/dto/response/product/ReadProductResponse.java index 48ba51892..c9346ef41 100644 --- a/src/main/java/gift/web/dto/response/product/ReadProductResponse.java +++ b/src/main/java/gift/web/dto/response/product/ReadProductResponse.java @@ -2,6 +2,8 @@ import gift.domain.Product; import gift.web.dto.response.category.ReadCategoryResponse; +import gift.web.dto.response.productoption.ReadProductOptionResponse; +import java.util.List; public class ReadProductResponse { @@ -9,19 +11,28 @@ public class ReadProductResponse { private final String name; private final Integer price; private final String imageUrl; + private final List options; private final ReadCategoryResponse category; - private ReadProductResponse(Long id, String name, Integer price, String imageUrl, - ReadCategoryResponse category) { + public ReadProductResponse(Long id, String name, Integer price, String imageUrl, + List options, ReadCategoryResponse category) { this.id = id; this.name = name; this.price = price; this.imageUrl = imageUrl; + this.options = options; this.category = category; } public static ReadProductResponse fromEntity(Product product) { - return new ReadProductResponse(product.getId(), product.getName(), product.getPrice(), product.getImageUrl().toString(), ReadCategoryResponse.fromEntity(product.getCategory())); + List productOptions = product.getProductOptions() + .stream() + .map(ReadProductOptionResponse::fromEntity) + .toList(); + + ReadCategoryResponse category = ReadCategoryResponse.fromEntity(product.getCategory()); + + return new ReadProductResponse(product.getId(), product.getName(), product.getPrice(), product.getImageUrl().toString(), productOptions, category); } public Long getId() { @@ -40,6 +51,10 @@ public String getImageUrl() { return imageUrl; } + public List getOptions() { + return options; + } + public ReadCategoryResponse getCategory() { return category; } diff --git a/src/main/resources/static/category.html b/src/main/resources/static/category.html new file mode 100644 index 000000000..e4686ff75 --- /dev/null +++ b/src/main/resources/static/category.html @@ -0,0 +1,1149 @@ + + + + + + + +카테고리 API + + + + + +
+
+

카테고리 API

+
+
+

카테고리 생성

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/categories' -i -X POST \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA' \
+    -d '{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}'
+
+
+
+
+

HTTP request

+
+
+
POST /api/categories HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA
+Content-Length: 147
+Host: localhost:8080
+
+{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 201 Created
+Location: http://localhost:8080/api/categories/1
+Content-Type: application/json
+Content-Length: 159
+
+{
+  "id" : 1,
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}' | http POST 'http://localhost:8080/api/categories' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

Request body

+
+
+
{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

name

String

카테고리명

description

String

카테고리 설명

imageUrl

String

이미지 URL

color

String

색상 코드

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

카테고리 ID

name

String

카테고리명

description

String

카테고리 설명

imageUrl

String

이미지 URL

color

String

색상 코드

+
+
+
+

카테고리 수정

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/categories/1' -i -X PUT \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA' \
+    -d '{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}'
+
+
+
+
+

HTTP request

+
+
+
PUT /api/categories/1 HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA
+Content-Length: 147
+Host: localhost:8080
+
+{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 159
+
+{
+  "id" : 1,
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}' | http PUT 'http://localhost:8080/api/categories/1' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

Request body

+
+
+
{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

name

String

카테고리명

description

String

카테고리 설명

imageUrl

String

이미지 URL

color

String

색상 코드

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

카테고리 ID

name

String

카테고리명

description

String

카테고리 설명

imageUrl

String

이미지 URL

color

String

색상 코드

+
+
+
+

카테고리 삭제

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/categories/1' -i -X DELETE \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

HTTP request

+
+
+
DELETE /api/categories/1 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 204 No Content
+
+
+
+
+

HTTPie request

+
+
+
$ http DELETE 'http://localhost:8080/api/categories/1' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
+
+
+
+
+
+

단일 카테고리 조회

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/categories/1' -i -X GET \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

HTTP request

+
+
+
GET /api/categories/1 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 159
+
+{
+  "id" : 1,
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/categories/1' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

카테고리 ID

name

String

카테고리명

description

String

카테고리 설명

imageUrl

String

이미지 URL

color

String

색상 코드

+
+
+
+

전체 카테고리 조회

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/categories' -i -X GET \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

HTTP request

+
+
+
GET /api/categories HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 369
+
+{
+  "categories" : [ {
+    "id" : 1,
+    "name" : "카테고리01",
+    "description" : "카테고리01 설명",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  }, {
+    "id" : 2,
+    "name" : "카테고리02",
+    "description" : "카테고리02 설명",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  } ]
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/categories' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

Query parameters

+ ++++ + + + + + + + + + + + + + + + + + + + + +
ParameterDescription

page

페이지 번호

size

페이지 크기

sort

정렬 조건

+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "categories" : [ {
+    "id" : 1,
+    "name" : "카테고리01",
+    "description" : "카테고리01 설명",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  }, {
+    "id" : 2,
+    "name" : "카테고리02",
+    "description" : "카테고리02 설명",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  } ]
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

categories[].id

Number

카테고리 ID

categories[].name

String

카테고리명

categories[].description

String

카테고리 설명

categories[].imageUrl

String

이미지 URL

categories[].color

String

색상 코드

+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs.html b/src/main/resources/static/docs.html new file mode 100644 index 000000000..a53ac9af9 --- /dev/null +++ b/src/main/resources/static/docs.html @@ -0,0 +1,3950 @@ + + + + + + + +카카오 테크 캠퍼스 STEP 2 - REST Docs + + + + + + +
+
+

상품 API

+
+
+

전체 상품 조회

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products?page=0&size=10' -i -X GET \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
GET /api/products?page=0&size=10 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 443
+
+{
+  "products" : [ {
+    "id" : 1,
+    "name" : "상품01",
+    "price" : 1000,
+    "imageUrl" : "https://via.placeholder.com/150",
+    "options" : [ {
+      "id" : 1,
+      "name" : "상품 옵션 01",
+      "stock" : 100
+    } ],
+    "category" : {
+      "id" : 1,
+      "name" : "카테고리01",
+      "description" : "카테고리01 입니다",
+      "imageUrl" : "https://via.placeholder.com/150",
+      "color" : "#FFFFFF"
+    }
+  } ]
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/products?page=0&size=10' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Query parameters

+ ++++ + + + + + + + + + + + + + + + + +
ParameterDescription

page

페이지 번호

size

페이지 크기

+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "products" : [ {
+    "id" : 1,
+    "name" : "상품01",
+    "price" : 1000,
+    "imageUrl" : "https://via.placeholder.com/150",
+    "options" : [ {
+      "id" : 1,
+      "name" : "상품 옵션 01",
+      "stock" : 100
+    } ],
+    "category" : {
+      "id" : 1,
+      "name" : "카테고리01",
+      "description" : "카테고리01 입니다",
+      "imageUrl" : "https://via.placeholder.com/150",
+      "color" : "#FFFFFF"
+    }
+  } ]
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

products[].id

Number

상품 ID

products[].name

String

상품명

products[].price

Number

상품 가격

products[].imageUrl

String

상품 이미지 URL

products[].options

Array

상품 옵션 목록

products[].options[].id

Number

상품 옵션 ID

products[].options[].name

String

상품 옵션명

products[].options[].stock

Number

상품 옵션 재고

products[].category.id

Number

카테고리 ID

products[].category.name

String

카테고리명

products[].category.description

String

카테고리 설명

products[].category.imageUrl

String

카테고리 이미지 URL

products[].category.color

String

카테고리 색상

+
+
+
+

단일 상품 조회

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1' -i -X GET \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
GET /api/products/1 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 386
+
+{
+  "id" : 1,
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "options" : [ {
+    "id" : 1,
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ],
+  "category" : {
+    "id" : 1,
+    "name" : "카테고리01",
+    "description" : "카테고리01 입니다",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  }
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/products/1' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "options" : [ {
+    "id" : 1,
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ],
+  "category" : {
+    "id" : 1,
+    "name" : "카테고리01",
+    "description" : "카테고리01 입니다",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  }
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

상품 ID

name

String

상품명

price

Number

상품 가격

imageUrl

String

상품 이미지 URL

options

Array

상품 옵션 목록

options[].id

Number

상품 옵션 ID

options[].name

String

상품 옵션명

options[].stock

Number

상품 옵션 재고

category.id

Number

카테고리 ID

category.name

String

카테고리명

category.description

String

카테고리 설명

category.imageUrl

String

카테고리 이미지 URL

category.color

String

카테고리 색상

+
+
+
+

상품 생성

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products' -i -X POST \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw' \
+    -d '{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "categoryId" : 1,
+  "productOptions" : [ {
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ]
+}'
+
+
+
+
+

HTTP request

+
+
+
POST /api/products HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Content-Length: 196
+Host: localhost:8080
+
+{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "categoryId" : 1,
+  "productOptions" : [ {
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ]
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 201 Created
+Location: http://localhost:8080/api/products/1
+Content-Type: application/json
+Content-Length: 216
+
+{
+  "id" : 1,
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "options" : [ {
+    "id" : 1,
+    "name" : "상품 옵션 01",
+    "stock" : 100,
+    "productId" : 1
+  } ]
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "categoryId" : 1,
+  "productOptions" : [ {
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ]
+}' | http POST 'http://localhost:8080/api/products' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "categoryId" : 1,
+  "productOptions" : [ {
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ]
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

name

String

상품명

price

Number

상품 가격

imageUrl

String

상품 이미지 URL

categoryId

Number

카테고리 ID

productOptions

Array

상품 옵션 목록

productOptions[].name

String

상품 옵션명

productOptions[].stock

Number

상품 옵션 재고

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "options" : [ {
+    "id" : 1,
+    "name" : "상품 옵션 01",
+    "stock" : 100,
+    "productId" : 1
+  } ]
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

상품 ID

name

String

상품명

price

Number

상품 가격

imageUrl

String

상품 이미지 URL

options

Array

상품 옵션 목록

options[].id

Number

상품 옵션 ID

options[].name

String

상품 옵션명

options[].stock

Number

상품 옵션 재고

options[].productId

Number

상품 옵션 상품 ID

+
+
+
+

상품 수정

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1' -i -X PUT \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw' \
+    -d '{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150"
+}'
+
+
+
+
+

HTTP request

+
+
+
PUT /api/products/1 HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Content-Length: 93
+Host: localhost:8080
+
+{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 296
+
+{
+  "id" : 1,
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "category" : {
+    "id" : 1,
+    "name" : "카테고리01",
+    "description" : "카테고리01 입니다",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  }
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150"
+}' | http PUT 'http://localhost:8080/api/products/1' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150"
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

name

String

상품명

price

Number

상품 가격

imageUrl

String

상품 이미지 URL

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "category" : {
+    "id" : 1,
+    "name" : "카테고리01",
+    "description" : "카테고리01 입니다",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  }
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

상품 ID

name

String

상품명

price

Number

상품 가격

imageUrl

String

상품 이미지 URL

category.id

Number

카테고리 ID

category.name

String

카테고리명

category.description

String

카테고리 설명

category.imageUrl

String

카테고리 이미지 URL

category.color

String

카테고리 색상

+
+
+
+

상품 삭제

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1' -i -X DELETE \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
DELETE /api/products/1 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 204 No Content
+
+
+
+
+

HTTPie request

+
+
+
$ http DELETE 'http://localhost:8080/api/products/1' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
+
+
+
+
+
+

상품 주문

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1/order' -i -X POST \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw' \
+    -d '{
+  "optionId" : 1,
+  "quantity" : 1,
+  "message" : "message"
+}'
+
+
+
+
+

HTTP request

+
+
+
POST /api/products/1/order HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Content-Length: 63
+Host: localhost:8080
+
+{
+  "optionId" : 1,
+  "quantity" : 1,
+  "message" : "message"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 83
+
+{
+  "productId" : 1,
+  "optionId" : 1,
+  "quantity" : 10,
+  "message" : "message"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "optionId" : 1,
+  "quantity" : 1,
+  "message" : "message"
+}' | http POST 'http://localhost:8080/api/products/1/order' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
{
+  "optionId" : 1,
+  "quantity" : 1,
+  "message" : "message"
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

optionId

Number

상품 옵션 ID

quantity

Number

주문 수량

message

String

메시지

+
+
+

Response body

+
+
+
{
+  "productId" : 1,
+  "optionId" : 1,
+  "quantity" : 10,
+  "message" : "message"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

productId

Number

상품 ID

optionId

Number

상품 옵션 ID

quantity

Number

주문 수량

message

String

메시지

+
+
+
+

위시 상품 추가

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/wish' -i -X POST \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw' \
+    -d '{
+  "productId" : 1,
+  "quantity" : 1
+}'
+
+
+
+
+

HTTP request

+
+
+
POST /api/products/wish HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Content-Length: 39
+Host: localhost:8080
+
+{
+  "productId" : 1,
+  "quantity" : 1
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 32
+
+{
+  "id" : 1,
+  "quantity" : 1
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "productId" : 1,
+  "quantity" : 1
+}' | http POST 'http://localhost:8080/api/products/wish' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
{
+  "productId" : 1,
+  "quantity" : 1
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

productId

Number

상품 ID

quantity

Number

수량

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "quantity" : 1
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

위시 상품 ID

quantity

Number

수량

+
+
+
+

상품 옵션 생성

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1/options' -i -X POST \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw' \
+    -d '{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}'
+
+
+
+
+

HTTP request

+
+
+
POST /api/products/1/options HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Content-Length: 50
+Host: localhost:8080
+
+{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 201 Created
+Location: http://localhost:8080/api/products/1/options/1
+Content-Type: application/json
+Content-Length: 62
+
+{
+  "id" : 1,
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}' | http POST 'http://localhost:8080/api/products/1/options' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

상품 옵션 ID

name

String

상품 옵션명

stock

Number

상품 옵션 재고

+
+
+
+

상품 옵션 조회

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1/options' -i -X GET \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
GET /api/products/1/options HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 92
+
+{
+  "options" : [ {
+    "id" : 1,
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ]
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/products/1/options' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "options" : [ {
+    "id" : 1,
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ]
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

options[].id

Number

상품 옵션 ID

options[].name

String

상품 옵션명

options[].stock

Number

상품 옵션 재고

+
+
+
+

상품 옵션 수정

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1/options/1' -i -X PUT \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw' \
+    -d '{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}'
+
+
+
+
+

HTTP request

+
+
+
PUT /api/products/1/options/1 HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Content-Length: 50
+Host: localhost:8080
+
+{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 62
+
+{
+  "id" : 1,
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}' | http PUT 'http://localhost:8080/api/products/1/options/1' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

name

String

상품 옵션명

stock

Number

상품 옵션 재고

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

상품 옵션 ID

name

String

상품 옵션명

stock

Number

상품 옵션 재고

+
+
+
+

상품 옵션 삭제

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1/options/1' -i -X DELETE \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
DELETE /api/products/1/options/1 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 204 No Content
+
+
+
+
+

HTTPie request

+
+
+
$ http DELETE 'http://localhost:8080/api/products/1/options/1' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
+
+
+
+
+
+
+
+

카테고리 API

+
+
+

카테고리 생성

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/categories' -i -X POST \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA' \
+    -d '{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}'
+
+
+
+
+

HTTP request

+
+
+
POST /api/categories HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA
+Content-Length: 147
+Host: localhost:8080
+
+{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 201 Created
+Location: http://localhost:8080/api/categories/1
+Content-Type: application/json
+Content-Length: 159
+
+{
+  "id" : 1,
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}' | http POST 'http://localhost:8080/api/categories' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

Request body

+
+
+
{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

name

String

카테고리명

description

String

카테고리 설명

imageUrl

String

이미지 URL

color

String

색상 코드

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

카테고리 ID

name

String

카테고리명

description

String

카테고리 설명

imageUrl

String

이미지 URL

color

String

색상 코드

+
+
+
+

카테고리 수정

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/categories/1' -i -X PUT \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA' \
+    -d '{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}'
+
+
+
+
+

HTTP request

+
+
+
PUT /api/categories/1 HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA
+Content-Length: 147
+Host: localhost:8080
+
+{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 159
+
+{
+  "id" : 1,
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}' | http PUT 'http://localhost:8080/api/categories/1' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

Request body

+
+
+
{
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

name

String

카테고리명

description

String

카테고리 설명

imageUrl

String

이미지 URL

color

String

색상 코드

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

카테고리 ID

name

String

카테고리명

description

String

카테고리 설명

imageUrl

String

이미지 URL

color

String

색상 코드

+
+
+
+

카테고리 삭제

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/categories/1' -i -X DELETE \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

HTTP request

+
+
+
DELETE /api/categories/1 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 204 No Content
+
+
+
+
+

HTTPie request

+
+
+
$ http DELETE 'http://localhost:8080/api/categories/1' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
+
+
+
+
+
+

단일 카테고리 조회

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/categories/1' -i -X GET \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

HTTP request

+
+
+
GET /api/categories/1 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 159
+
+{
+  "id" : 1,
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/categories/1' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "카테고리01",
+  "description" : "카테고리01 설명",
+  "imageUrl" : "https://via.placeholder.com/150",
+  "color" : "#FFFFFF"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

카테고리 ID

name

String

카테고리명

description

String

카테고리 설명

imageUrl

String

이미지 URL

color

String

색상 코드

+
+
+
+

전체 카테고리 조회

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/categories' -i -X GET \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

HTTP request

+
+
+
GET /api/categories HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 369
+
+{
+  "categories" : [ {
+    "id" : 1,
+    "name" : "카테고리01",
+    "description" : "카테고리01 설명",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  }, {
+    "id" : 2,
+    "name" : "카테고리02",
+    "description" : "카테고리02 설명",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  } ]
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/categories' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTAyLCJleHAiOjE3MjIzNTg3MDJ9.Ui6qD8vxUswbaz_RdFW-pU7XmGPFxaXS0DU7FBlBJwB_-yuL9WVCc7bq5yOezJltGK77L6tu3653lFLyt6zZsA'
+
+
+
+
+

Query parameters

+ ++++ + + + + + + + + + + + + + + + + + + + + +
ParameterDescription

page

페이지 번호

size

페이지 크기

sort

정렬 조건

+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "categories" : [ {
+    "id" : 1,
+    "name" : "카테고리01",
+    "description" : "카테고리01 설명",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  }, {
+    "id" : 2,
+    "name" : "카테고리02",
+    "description" : "카테고리02 설명",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  } ]
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

categories[].id

Number

카테고리 ID

categories[].name

String

카테고리명

categories[].description

String

카테고리 설명

categories[].imageUrl

String

이미지 URL

categories[].color

String

색상 코드

+
+
+
+
+
+

회원 API

+
+
+

회원가입

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/members/register' -i -X POST \
+    -H 'Content-Type: application/json' \
+    -d '{
+  "email" : "member01@gmail.com",
+  "password" : "password01",
+  "name" : "member01"
+}'
+
+
+
+
+

HTTP request

+
+
+
POST /api/members/register HTTP/1.1
+Content-Type: application/json
+Content-Length: 88
+Host: localhost:8080
+
+{
+  "email" : "member01@gmail.com",
+  "password" : "password01",
+  "name" : "member01"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 201 Created
+Location: http://localhost:8080/api/members/1
+Content-Type: application/json
+Content-Length: 71
+
+{
+  "id" : 1,
+  "email" : "member01@gmail.com",
+  "name" : "member01"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "email" : "member01@gmail.com",
+  "password" : "password01",
+  "name" : "member01"
+}' | http POST 'http://localhost:8080/api/members/register' \
+    'Content-Type:application/json'
+
+
+
+
+

Request body

+
+
+
{
+  "email" : "member01@gmail.com",
+  "password" : "password01",
+  "name" : "member01"
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

email

String

이메일

password

String

비밀번호

name

String

이름

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "email" : "member01@gmail.com",
+  "name" : "member01"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

회원 식별자

email

String

이메일

name

String

이름

+
+
+
+

로그인

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/members/login' -i -X POST \
+    -H 'Content-Type: application/json' \
+    -d '{
+  "email" : "member01@gmail.com",
+  "password" : "password01"
+}'
+
+
+
+
+

HTTP request

+
+
+
POST /api/members/login HTTP/1.1
+Content-Type: application/json
+Content-Length: 65
+Host: localhost:8080
+
+{
+  "email" : "member01@gmail.com",
+  "password" : "password01"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 196
+
+{
+  "accessToken" : "eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "email" : "member01@gmail.com",
+  "password" : "password01"
+}' | http POST 'http://localhost:8080/api/members/login' \
+    'Content-Type:application/json'
+
+
+
+
+

Request body

+
+
+
{
+  "email" : "member01@gmail.com",
+  "password" : "password01"
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

email

String

이메일

password

String

비밀번호

+
+
+

Response body

+
+
+
{
+  "accessToken" : "eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + +
PathTypeDescription

accessToken

String

Bearer Token

+
+
+
+

회원 조회

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/members/1' -i -X GET \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
GET /api/members/1 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 100
+
+{
+  "id" : 1,
+  "email" : "member01@gmail.com",
+  "password" : "password01",
+  "name" : "member01"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/members/1' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "email" : "member01@gmail.com",
+  "password" : "password01",
+  "name" : "member01"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

회원 식별자

email

String

이메일

password

String

비밀번호

name

String

이름

+
+
+
+

위시 상품 조회

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/members/wishlist' -i -X GET \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
GET /api/members/wishlist HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 180
+
+{
+  "wishlist" : [ {
+    "id" : 1,
+    "productId" : 1,
+    "name" : "product01",
+    "price" : 1000,
+    "quantity" : 5,
+    "imageUrl" : "https://via.placeholder.com/150"
+  } ]
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/members/wishlist' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Query parameters

+ ++++ + + + + + + + + + + + + + + + + +
ParameterDescription

page

페이지 번호

size

페이지 크기

+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "wishlist" : [ {
+    "id" : 1,
+    "productId" : 1,
+    "name" : "product01",
+    "price" : 1000,
+    "quantity" : 5,
+    "imageUrl" : "https://via.placeholder.com/150"
+  } ]
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

wishlist[].id

Number

위시 상품 식별자

wishlist[].productId

Number

상품 식별자

wishlist[].name

String

상품명

wishlist[].price

Number

가격

wishlist[].quantity

Number

재고 수량

wishlist[].imageUrl

String

이미지 URL

+
+
+
+

위시 상품 수정

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/members/wishlist/1' -i -X PUT \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw' \
+    -d '{
+  "quantity" : 5
+}'
+
+
+
+
+

HTTP request

+
+
+
PUT /api/members/wishlist/1 HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Content-Length: 20
+Host: localhost:8080
+
+{
+  "quantity" : 5
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 143
+
+{
+  "id" : 1,
+  "productId" : 1,
+  "name" : "product01",
+  "price" : 1000,
+  "quantity" : 5,
+  "imageUrl" : "https://via.placeholder.com/150"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "quantity" : 5
+}' | http PUT 'http://localhost:8080/api/members/wishlist/1' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
{
+  "quantity" : 5
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + +
PathTypeDescription

quantity

Number

수량

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "productId" : 1,
+  "name" : "product01",
+  "price" : 1000,
+  "quantity" : 5,
+  "imageUrl" : "https://via.placeholder.com/150"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

위시 상품 식별자

productId

Number

상품 식별자

name

String

상품명

price

Number

가격

quantity

Number

재고 수량

imageUrl

String

이미지 URL

+
+
+
+

위시 상품 삭제

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/members/wishlist/1' -i -X DELETE \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
DELETE /api/members/wishlist/1 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 204 No Content
+
+
+
+
+

HTTPie request

+
+
+
$ http DELETE 'http://localhost:8080/api/members/wishlist/1' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
+
+
+
+
+
+
+
+

소셜 로그인 API

+
+
+

카카오 로그인

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/login/oauth2/kakao?code=kakao-auth-code' -i -X GET
+
+
+
+
+

HTTP request

+
+
+
GET /api/login/oauth2/kakao?code=kakao-auth-code HTTP/1.1
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 47
+
+{
+  "accessToken" : "Bearer {{access_token}}"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/login/oauth2/kakao?code=kakao-auth-code'
+
+
+
+
+

Query parameters

+ ++++ + + + + + + + + + + + + +
ParameterDescription

code

kakao-auth-code

+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "accessToken" : "Bearer {{access_token}}"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + +
PathTypeDescription

accessToken

class java.lang.String

access-token(BEARER)

+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/main/resources/static/js/script.js b/src/main/resources/static/js/script.js index 9fed7fed9..98b615c83 100644 --- a/src/main/resources/static/js/script.js +++ b/src/main/resources/static/js/script.js @@ -140,44 +140,6 @@ function giftLogin() { }); } -function kakaoLogin() { - fetch('https://kauth.kakao.com/oauth/authorize?client_id=c14cc9f825429533e917e1b1be966e08&redirect_uri=http://localhost:8080/api/login/oauth2/kakao&response_type=code') - .then(response => { - if (!response.ok) { - return response.json().then(errorData => { - throw new Error(errorData.description); - }); - } - return response.json(); - }) - .then(data => { - console.log(data); - localStorage.setItem('accessToken', data.accessToken); - - fetch('/view/login-callback', { - method: 'GET', - headers: { - 'Authorization': 'Bearer ' + data.accessToken - } - }) - .then(response => { - if (!response.ok) { - throw new Error('페이지 로드 실패: ' + response.statusText); - } - return response.text(); - }) - .then(html => { - document.write(html); - }) - .catch(error => { - console.error('페이지 로드 실패: ', error); - }); - }) - .catch(error => { - console.error('알 수 없는 에러가 발생했습니다! ', error); - }); -} - function registerUser() { const form = document.getElementById('registerForm'); const formData = new FormData(form); diff --git a/src/main/resources/static/login.html b/src/main/resources/static/login.html new file mode 100644 index 000000000..1c3079433 --- /dev/null +++ b/src/main/resources/static/login.html @@ -0,0 +1,561 @@ + + + + + + + +소셜 로그인 API + + + + + +
+
+

소셜 로그인 API

+
+
+

카카오 로그인

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/login/oauth2/kakao?code=kakao-auth-code' -i -X GET
+
+
+
+
+

HTTP request

+
+
+
GET /api/login/oauth2/kakao?code=kakao-auth-code HTTP/1.1
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 47
+
+{
+  "accessToken" : "Bearer {{access_token}}"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/login/oauth2/kakao?code=kakao-auth-code'
+
+
+
+
+

Query parameters

+ ++++ + + + + + + + + + + + + +
ParameterDescription

code

kakao-auth-code

+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "accessToken" : "Bearer {{access_token}}"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + +
PathTypeDescription

accessToken

class java.lang.String

access-token(BEARER)

+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/member.html b/src/main/resources/static/member.html new file mode 100644 index 000000000..d64053781 --- /dev/null +++ b/src/main/resources/static/member.html @@ -0,0 +1,1219 @@ + + + + + + + +회원 API + + + + + +
+
+

회원 API

+
+
+

회원가입

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/members/register' -i -X POST \
+    -H 'Content-Type: application/json' \
+    -d '{
+  "email" : "member01@gmail.com",
+  "password" : "password01",
+  "name" : "member01"
+}'
+
+
+
+
+

HTTP request

+
+
+
POST /api/members/register HTTP/1.1
+Content-Type: application/json
+Content-Length: 88
+Host: localhost:8080
+
+{
+  "email" : "member01@gmail.com",
+  "password" : "password01",
+  "name" : "member01"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 201 Created
+Location: http://localhost:8080/api/members/1
+Content-Type: application/json
+Content-Length: 71
+
+{
+  "id" : 1,
+  "email" : "member01@gmail.com",
+  "name" : "member01"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "email" : "member01@gmail.com",
+  "password" : "password01",
+  "name" : "member01"
+}' | http POST 'http://localhost:8080/api/members/register' \
+    'Content-Type:application/json'
+
+
+
+
+

Request body

+
+
+
{
+  "email" : "member01@gmail.com",
+  "password" : "password01",
+  "name" : "member01"
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

email

String

이메일

password

String

비밀번호

name

String

이름

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "email" : "member01@gmail.com",
+  "name" : "member01"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

회원 식별자

email

String

이메일

name

String

이름

+
+
+
+

로그인

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/members/login' -i -X POST \
+    -H 'Content-Type: application/json' \
+    -d '{
+  "email" : "member01@gmail.com",
+  "password" : "password01"
+}'
+
+
+
+
+

HTTP request

+
+
+
POST /api/members/login HTTP/1.1
+Content-Type: application/json
+Content-Length: 65
+Host: localhost:8080
+
+{
+  "email" : "member01@gmail.com",
+  "password" : "password01"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 196
+
+{
+  "accessToken" : "eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "email" : "member01@gmail.com",
+  "password" : "password01"
+}' | http POST 'http://localhost:8080/api/members/login' \
+    'Content-Type:application/json'
+
+
+
+
+

Request body

+
+
+
{
+  "email" : "member01@gmail.com",
+  "password" : "password01"
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

email

String

이메일

password

String

비밀번호

+
+
+

Response body

+
+
+
{
+  "accessToken" : "eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + +
PathTypeDescription

accessToken

String

Bearer Token

+
+
+
+

회원 조회

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/members/1' -i -X GET \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
GET /api/members/1 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 100
+
+{
+  "id" : 1,
+  "email" : "member01@gmail.com",
+  "password" : "password01",
+  "name" : "member01"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/members/1' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "email" : "member01@gmail.com",
+  "password" : "password01",
+  "name" : "member01"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

회원 식별자

email

String

이메일

password

String

비밀번호

name

String

이름

+
+
+
+

위시 상품 조회

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/members/wishlist' -i -X GET \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
GET /api/members/wishlist HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 180
+
+{
+  "wishlist" : [ {
+    "id" : 1,
+    "productId" : 1,
+    "name" : "product01",
+    "price" : 1000,
+    "quantity" : 5,
+    "imageUrl" : "https://via.placeholder.com/150"
+  } ]
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/members/wishlist' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Query parameters

+ ++++ + + + + + + + + + + + + + + + + +
ParameterDescription

page

페이지 번호

size

페이지 크기

+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "wishlist" : [ {
+    "id" : 1,
+    "productId" : 1,
+    "name" : "product01",
+    "price" : 1000,
+    "quantity" : 5,
+    "imageUrl" : "https://via.placeholder.com/150"
+  } ]
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

wishlist[].id

Number

위시 상품 식별자

wishlist[].productId

Number

상품 식별자

wishlist[].name

String

상품명

wishlist[].price

Number

가격

wishlist[].quantity

Number

재고 수량

wishlist[].imageUrl

String

이미지 URL

+
+
+
+

위시 상품 수정

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/members/wishlist/1' -i -X PUT \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw' \
+    -d '{
+  "quantity" : 5
+}'
+
+
+
+
+

HTTP request

+
+
+
PUT /api/members/wishlist/1 HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Content-Length: 20
+Host: localhost:8080
+
+{
+  "quantity" : 5
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 143
+
+{
+  "id" : 1,
+  "productId" : 1,
+  "name" : "product01",
+  "price" : 1000,
+  "quantity" : 5,
+  "imageUrl" : "https://via.placeholder.com/150"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "quantity" : 5
+}' | http PUT 'http://localhost:8080/api/members/wishlist/1' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
{
+  "quantity" : 5
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + +
PathTypeDescription

quantity

Number

수량

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "productId" : 1,
+  "name" : "product01",
+  "price" : 1000,
+  "quantity" : 5,
+  "imageUrl" : "https://via.placeholder.com/150"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

위시 상품 식별자

productId

Number

상품 식별자

name

String

상품명

price

Number

가격

quantity

Number

재고 수량

imageUrl

String

이미지 URL

+
+
+
+

위시 상품 삭제

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/members/wishlist/1' -i -X DELETE \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
DELETE /api/members/wishlist/1 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 204 No Content
+
+
+
+
+

HTTPie request

+
+
+
$ http DELETE 'http://localhost:8080/api/members/wishlist/1' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/product.html b/src/main/resources/static/product.html new file mode 100644 index 000000000..93f25fe52 --- /dev/null +++ b/src/main/resources/static/product.html @@ -0,0 +1,2078 @@ + + + + + + + +상품 API + + + + + +
+
+

상품 API

+
+
+

전체 상품 조회

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products?page=0&size=10' -i -X GET \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
GET /api/products?page=0&size=10 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 443
+
+{
+  "products" : [ {
+    "id" : 1,
+    "name" : "상품01",
+    "price" : 1000,
+    "imageUrl" : "https://via.placeholder.com/150",
+    "options" : [ {
+      "id" : 1,
+      "name" : "상품 옵션 01",
+      "stock" : 100
+    } ],
+    "category" : {
+      "id" : 1,
+      "name" : "카테고리01",
+      "description" : "카테고리01 입니다",
+      "imageUrl" : "https://via.placeholder.com/150",
+      "color" : "#FFFFFF"
+    }
+  } ]
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/products?page=0&size=10' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Query parameters

+ ++++ + + + + + + + + + + + + + + + + +
ParameterDescription

page

페이지 번호

size

페이지 크기

+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "products" : [ {
+    "id" : 1,
+    "name" : "상품01",
+    "price" : 1000,
+    "imageUrl" : "https://via.placeholder.com/150",
+    "options" : [ {
+      "id" : 1,
+      "name" : "상품 옵션 01",
+      "stock" : 100
+    } ],
+    "category" : {
+      "id" : 1,
+      "name" : "카테고리01",
+      "description" : "카테고리01 입니다",
+      "imageUrl" : "https://via.placeholder.com/150",
+      "color" : "#FFFFFF"
+    }
+  } ]
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

products[].id

Number

상품 ID

products[].name

String

상품명

products[].price

Number

상품 가격

products[].imageUrl

String

상품 이미지 URL

products[].options

Array

상품 옵션 목록

products[].options[].id

Number

상품 옵션 ID

products[].options[].name

String

상품 옵션명

products[].options[].stock

Number

상품 옵션 재고

products[].category.id

Number

카테고리 ID

products[].category.name

String

카테고리명

products[].category.description

String

카테고리 설명

products[].category.imageUrl

String

카테고리 이미지 URL

products[].category.color

String

카테고리 색상

+
+
+
+

단일 상품 조회

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1' -i -X GET \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
GET /api/products/1 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 386
+
+{
+  "id" : 1,
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "options" : [ {
+    "id" : 1,
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ],
+  "category" : {
+    "id" : 1,
+    "name" : "카테고리01",
+    "description" : "카테고리01 입니다",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  }
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/products/1' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "options" : [ {
+    "id" : 1,
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ],
+  "category" : {
+    "id" : 1,
+    "name" : "카테고리01",
+    "description" : "카테고리01 입니다",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  }
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

상품 ID

name

String

상품명

price

Number

상품 가격

imageUrl

String

상품 이미지 URL

options

Array

상품 옵션 목록

options[].id

Number

상품 옵션 ID

options[].name

String

상품 옵션명

options[].stock

Number

상품 옵션 재고

category.id

Number

카테고리 ID

category.name

String

카테고리명

category.description

String

카테고리 설명

category.imageUrl

String

카테고리 이미지 URL

category.color

String

카테고리 색상

+
+
+
+

상품 생성

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products' -i -X POST \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw' \
+    -d '{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "categoryId" : 1,
+  "productOptions" : [ {
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ]
+}'
+
+
+
+
+

HTTP request

+
+
+
POST /api/products HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Content-Length: 196
+Host: localhost:8080
+
+{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "categoryId" : 1,
+  "productOptions" : [ {
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ]
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 201 Created
+Location: http://localhost:8080/api/products/1
+Content-Type: application/json
+Content-Length: 216
+
+{
+  "id" : 1,
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "options" : [ {
+    "id" : 1,
+    "name" : "상품 옵션 01",
+    "stock" : 100,
+    "productId" : 1
+  } ]
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "categoryId" : 1,
+  "productOptions" : [ {
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ]
+}' | http POST 'http://localhost:8080/api/products' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "categoryId" : 1,
+  "productOptions" : [ {
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ]
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

name

String

상품명

price

Number

상품 가격

imageUrl

String

상품 이미지 URL

categoryId

Number

카테고리 ID

productOptions

Array

상품 옵션 목록

productOptions[].name

String

상품 옵션명

productOptions[].stock

Number

상품 옵션 재고

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "options" : [ {
+    "id" : 1,
+    "name" : "상품 옵션 01",
+    "stock" : 100,
+    "productId" : 1
+  } ]
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

상품 ID

name

String

상품명

price

Number

상품 가격

imageUrl

String

상품 이미지 URL

options

Array

상품 옵션 목록

options[].id

Number

상품 옵션 ID

options[].name

String

상품 옵션명

options[].stock

Number

상품 옵션 재고

options[].productId

Number

상품 옵션 상품 ID

+
+
+
+

상품 수정

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1' -i -X PUT \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw' \
+    -d '{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150"
+}'
+
+
+
+
+

HTTP request

+
+
+
PUT /api/products/1 HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Content-Length: 93
+Host: localhost:8080
+
+{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 296
+
+{
+  "id" : 1,
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "category" : {
+    "id" : 1,
+    "name" : "카테고리01",
+    "description" : "카테고리01 입니다",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  }
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150"
+}' | http PUT 'http://localhost:8080/api/products/1' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
{
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150"
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

name

String

상품명

price

Number

상품 가격

imageUrl

String

상품 이미지 URL

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "상품01",
+  "price" : 1000,
+  "imageUrl" : "https://via.placeholder.com/150",
+  "category" : {
+    "id" : 1,
+    "name" : "카테고리01",
+    "description" : "카테고리01 입니다",
+    "imageUrl" : "https://via.placeholder.com/150",
+    "color" : "#FFFFFF"
+  }
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

상품 ID

name

String

상품명

price

Number

상품 가격

imageUrl

String

상품 이미지 URL

category.id

Number

카테고리 ID

category.name

String

카테고리명

category.description

String

카테고리 설명

category.imageUrl

String

카테고리 이미지 URL

category.color

String

카테고리 색상

+
+
+
+

상품 삭제

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1' -i -X DELETE \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
DELETE /api/products/1 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 204 No Content
+
+
+
+
+

HTTPie request

+
+
+
$ http DELETE 'http://localhost:8080/api/products/1' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
+
+
+
+
+
+

상품 주문

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1/order' -i -X POST \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw' \
+    -d '{
+  "optionId" : 1,
+  "quantity" : 1,
+  "message" : "message"
+}'
+
+
+
+
+

HTTP request

+
+
+
POST /api/products/1/order HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Content-Length: 63
+Host: localhost:8080
+
+{
+  "optionId" : 1,
+  "quantity" : 1,
+  "message" : "message"
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 83
+
+{
+  "productId" : 1,
+  "optionId" : 1,
+  "quantity" : 10,
+  "message" : "message"
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "optionId" : 1,
+  "quantity" : 1,
+  "message" : "message"
+}' | http POST 'http://localhost:8080/api/products/1/order' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
{
+  "optionId" : 1,
+  "quantity" : 1,
+  "message" : "message"
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

optionId

Number

상품 옵션 ID

quantity

Number

주문 수량

message

String

메시지

+
+
+

Response body

+
+
+
{
+  "productId" : 1,
+  "optionId" : 1,
+  "quantity" : 10,
+  "message" : "message"
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

productId

Number

상품 ID

optionId

Number

상품 옵션 ID

quantity

Number

주문 수량

message

String

메시지

+
+
+
+

위시 상품 추가

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/wish' -i -X POST \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw' \
+    -d '{
+  "productId" : 1,
+  "quantity" : 1
+}'
+
+
+
+
+

HTTP request

+
+
+
POST /api/products/wish HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Content-Length: 39
+Host: localhost:8080
+
+{
+  "productId" : 1,
+  "quantity" : 1
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 32
+
+{
+  "id" : 1,
+  "quantity" : 1
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "productId" : 1,
+  "quantity" : 1
+}' | http POST 'http://localhost:8080/api/products/wish' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
{
+  "productId" : 1,
+  "quantity" : 1
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

productId

Number

상품 ID

quantity

Number

수량

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "quantity" : 1
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

위시 상품 ID

quantity

Number

수량

+
+
+
+

상품 옵션 생성

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1/options' -i -X POST \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw' \
+    -d '{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}'
+
+
+
+
+

HTTP request

+
+
+
POST /api/products/1/options HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Content-Length: 50
+Host: localhost:8080
+
+{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 201 Created
+Location: http://localhost:8080/api/products/1/options/1
+Content-Type: application/json
+Content-Length: 62
+
+{
+  "id" : 1,
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}' | http POST 'http://localhost:8080/api/products/1/options' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

상품 옵션 ID

name

String

상품 옵션명

stock

Number

상품 옵션 재고

+
+
+
+

상품 옵션 조회

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1/options' -i -X GET \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
GET /api/products/1/options HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 92
+
+{
+  "options" : [ {
+    "id" : 1,
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ]
+}
+
+
+
+
+

HTTPie request

+
+
+
$ http GET 'http://localhost:8080/api/products/1/options' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
{
+  "options" : [ {
+    "id" : 1,
+    "name" : "상품 옵션 01",
+    "stock" : 100
+  } ]
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

options[].id

Number

상품 옵션 ID

options[].name

String

상품 옵션명

options[].stock

Number

상품 옵션 재고

+
+
+
+

상품 옵션 수정

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1/options/1' -i -X PUT \
+    -H 'Content-Type: application/json' \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw' \
+    -d '{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}'
+
+
+
+
+

HTTP request

+
+
+
PUT /api/products/1/options/1 HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Content-Length: 50
+Host: localhost:8080
+
+{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 62
+
+{
+  "id" : 1,
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

HTTPie request

+
+
+
$ echo '{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}' | http PUT 'http://localhost:8080/api/products/1/options/1' \
+    'Content-Type:application/json' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
{
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

name

String

상품 옵션명

stock

Number

상품 옵션 재고

+
+
+

Response body

+
+
+
{
+  "id" : 1,
+  "name" : "상품 옵션 01",
+  "stock" : 100
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

상품 옵션 ID

name

String

상품 옵션명

stock

Number

상품 옵션 재고

+
+
+
+

상품 옵션 삭제

+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/api/products/1/options/1' -i -X DELETE \
+    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

HTTP request

+
+
+
DELETE /api/products/1/options/1 HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw
+Host: localhost:8080
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 204 No Content
+
+
+
+
+

HTTPie request

+
+
+
$ http DELETE 'http://localhost:8080/api/products/1/options/1' \
+    'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJtZW1iZXJJZCI6MSwiaWF0IjoxNzIyMzU1MTA0LCJleHAiOjE3MjIzNTg3MDR9.89Yf45yjhDeU0k6CYLtjklTXJTJZ8K1aDtqgdLnpIzLkpfVMlu-JX84iwIzn47WsywraIJ-7AsJSS_5zs4TaQw'
+
+
+
+
+

Request body

+
+
+
+
+
+
+
+

Response body

+
+
+
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/form/login-form.html b/src/main/resources/templates/form/login-form.html index eeb3b6e3f..ac13998d2 100644 --- a/src/main/resources/templates/form/login-form.html +++ b/src/main/resources/templates/form/login-form.html @@ -23,7 +23,7 @@

로그인


diff --git a/src/test/java/gift/config/RestDocsConfiguration.java b/src/test/java/gift/config/RestDocsConfiguration.java new file mode 100644 index 000000000..8682ea2d6 --- /dev/null +++ b/src/test/java/gift/config/RestDocsConfiguration.java @@ -0,0 +1,21 @@ +package gift.config; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.operation.preprocess.Preprocessors; + +@TestConfiguration +public class RestDocsConfiguration { + + @Bean + public RestDocumentationResultHandler write() { + return MockMvcRestDocumentation.document( + "{class-name}/{method-name}", + Preprocessors.preprocessRequest(Preprocessors.prettyPrint()), //예쁘게 출력 + Preprocessors.preprocessResponse(Preprocessors.prettyPrint()) //예쁘게 출력 + ); + } + +} diff --git a/src/test/java/gift/mock/MockLoginMemberArgumentResolver.java b/src/test/java/gift/mock/MockLoginMemberArgumentResolver.java new file mode 100644 index 000000000..5829a7d93 --- /dev/null +++ b/src/test/java/gift/mock/MockLoginMemberArgumentResolver.java @@ -0,0 +1,25 @@ +package gift.mock; + +import gift.domain.constants.Platform; +import gift.domain.vo.Email; +import gift.web.dto.MemberDetails; +import gift.web.resolver.LoginMemberArgumentResolver; +import org.springframework.boot.test.context.TestComponent; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; + +@TestComponent +public class MockLoginMemberArgumentResolver extends LoginMemberArgumentResolver { + + public MockLoginMemberArgumentResolver() { + super(null, null); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + return new MemberDetails(1L, Email.from("member01@gmail.com"), Platform.GIFT); + } +} diff --git a/src/test/java/gift/service/ProductOptionServiceTest.java b/src/test/java/gift/service/ProductOptionServiceTest.java index 69b413f0c..da01416cd 100644 --- a/src/test/java/gift/service/ProductOptionServiceTest.java +++ b/src/test/java/gift/service/ProductOptionServiceTest.java @@ -4,13 +4,14 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import gift.domain.Product; import gift.domain.ProductOption; import gift.domain.ProductOption.Builder; import gift.repository.ProductOptionRepository; +import gift.repository.ProductRepository; import gift.web.dto.request.productoption.CreateProductOptionRequest; import gift.web.dto.request.productoption.SubtractProductOptionQuantityRequest; import gift.web.dto.request.productoption.UpdateProductOptionRequest; @@ -37,6 +38,9 @@ class ProductOptionServiceTest { @Mock private ProductOptionRepository productOptionRepository; + @Mock + private ProductRepository productRepository; + @Test @DisplayName("상품 옵션 생성 요청이 정상적일 때, 상품 옵션을 성공적으로 생성합니다.") void createOption() { @@ -44,6 +48,7 @@ void createOption() { Long productId = 1L; CreateProductOptionRequest request = new CreateProductOptionRequest("optionName", 1000); given(productOptionRepository.save(any())).willReturn(request.toEntity(productId)); + given(productRepository.findById(any())).willReturn(Optional.of(new Product.Builder().productOptions(List.of(request.toEntity(productId))).build())); //when CreateProductOptionResponse response = productOptionService.createOption(productId, request); diff --git a/src/test/java/gift/service/ProductServiceTest.java b/src/test/java/gift/service/ProductServiceTest.java index ad43d05ca..c8332d0c2 100644 --- a/src/test/java/gift/service/ProductServiceTest.java +++ b/src/test/java/gift/service/ProductServiceTest.java @@ -55,7 +55,7 @@ class ProductServiceTest { @BeforeEach void setUp() { - productOptionService = new ProductOptionService(productOptionRepository); + productOptionService = new ProductOptionService(productOptionRepository, productRepository); productService = new ProductService(productRepository, categoryRepository, wishProductRepository, productOptionService); } diff --git a/src/test/java/gift/web/controller/api/CategoryApiControllerTest.java b/src/test/java/gift/web/controller/api/CategoryApiControllerTest.java new file mode 100644 index 000000000..20563b011 --- /dev/null +++ b/src/test/java/gift/web/controller/api/CategoryApiControllerTest.java @@ -0,0 +1,237 @@ +package gift.web.controller.api; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import gift.authentication.token.JwtProvider; +import gift.config.RestDocsConfiguration; +import gift.domain.Member; +import gift.domain.Member.Builder; +import gift.domain.vo.Email; +import gift.service.CategoryService; +import gift.web.dto.request.category.CreateCategoryRequest; +import gift.web.dto.request.category.UpdateCategoryRequest; +import gift.web.dto.response.category.CreateCategoryResponse; +import gift.web.dto.response.category.ReadAllCategoriesResponse; +import gift.web.dto.response.category.ReadCategoryResponse; +import gift.web.dto.response.category.UpdateCategoryResponse; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +@ActiveProfiles("test") +@SpringBootTest +@Import(RestDocsConfiguration.class) +@ExtendWith(RestDocumentationExtension.class) +class CategoryApiControllerTest { + + private MockMvc mockMvc; + + @Autowired + protected RestDocumentationResultHandler restDocs; + + @Autowired + protected ObjectMapper objectMapper; + + @Autowired + private JwtProvider jwtProvider; + + @MockBean + private CategoryService categoryService; + + private String accessToken; + + private static final String BASE_URL = "/api/categories"; + + @BeforeEach + void setUp( + final RestDocumentationContextProvider provider + ) { + mockMvc = MockMvcBuilders + .standaloneSetup(new CategoryApiController(categoryService)) + .setCustomArgumentResolvers(new PageableHandlerMethodArgumentResolver()) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) + .alwaysDo(restDocs) + .build(); + + Member member = new Builder().id(1L).name("회원01").email(Email.from("member01@gmail.com")) + .build(); + accessToken = jwtProvider.generateToken(member).getValue(); + } + + + @Test + @DisplayName("카테고리 생성") + void createCategory() throws Exception { + CreateCategoryRequest request = new CreateCategoryRequest("카테고리01", + "카테고리01 설명", "https://via.placeholder.com/150", "#FFFFFF"); + + String content = objectMapper.writeValueAsString(request); + + given(categoryService.createCategory(any(CreateCategoryRequest.class))) + .willReturn(new CreateCategoryResponse(1L, "카테고리01", "카테고리01 설명", "https://via.placeholder.com/150", "#FFFFFF")); + + mockMvc + .perform( + post(BASE_URL) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .content(content) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("카테고리명"), + fieldWithPath("description").type(JsonFieldType.STRING).description("카테고리 설명"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("이미지 URL"), + fieldWithPath("color").type(JsonFieldType.STRING).description("색상 코드") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("카테고리 ID"), + fieldWithPath("name").type(JsonFieldType.STRING).description("카테고리명"), + fieldWithPath("description").type(JsonFieldType.STRING).description("카테고리 설명"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("이미지 URL"), + fieldWithPath("color").type(JsonFieldType.STRING).description("색상 코드") + ) + ) + ); + } + + @Test + @DisplayName("모든 카테고리 조회") + void readAllCategories() throws Exception { + given(categoryService.readAllCategories(any())) + .willReturn(new ReadAllCategoriesResponse(List.of( + new ReadCategoryResponse(1L, "카테고리01", "카테고리01 설명", "https://via.placeholder.com/150", "#FFFFFF"), + new ReadCategoryResponse(2L, "카테고리02", "카테고리02 설명", "https://via.placeholder.com/150", "#FFFFFF") + ))); + + mockMvc + .perform( + get(BASE_URL) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + queryParameters( + parameterWithName("page").optional().description("페이지 번호"), + parameterWithName("size").optional().description("페이지 크기"), + parameterWithName("sort").optional().description("정렬 조건") + ), + responseFields( + fieldWithPath("categories[].id").type(JsonFieldType.NUMBER).description("카테고리 ID"), + fieldWithPath("categories[].name").type(JsonFieldType.STRING).description("카테고리명"), + fieldWithPath("categories[].description").type(JsonFieldType.STRING).description("카테고리 설명"), + fieldWithPath("categories[].imageUrl").type(JsonFieldType.STRING).description("이미지 URL"), + fieldWithPath("categories[].color").type(JsonFieldType.STRING).description("색상 코드") + ) + ) + ); + } + + @Test + @DisplayName("단일 카테고리 조회") + void readCategory() throws Exception { + given(categoryService.readCategory(any(Long.class))) + .willReturn(new ReadCategoryResponse(1L, "카테고리01", "카테고리01 설명", "https://via.placeholder.com/150", "#FFFFFF")); + + mockMvc + .perform( + get(BASE_URL + "/{id}", 1L) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("카테고리 ID"), + fieldWithPath("name").type(JsonFieldType.STRING).description("카테고리명"), + fieldWithPath("description").type(JsonFieldType.STRING).description("카테고리 설명"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("이미지 URL"), + fieldWithPath("color").type(JsonFieldType.STRING).description("색상 코드") + ) + ) + ); + } + + @Test + @DisplayName("카테고리 수정") + void updateCategory() throws Exception { + UpdateCategoryRequest request = new UpdateCategoryRequest("카테고리01", "카테고리01 설명", + "https://via.placeholder.com/150", "#FFFFFF"); + + String content = objectMapper.writeValueAsString(request); + + given(categoryService.updateCategory(any(Long.class), any(UpdateCategoryRequest.class))) + .willReturn(new UpdateCategoryResponse(1L, "카테고리01", "카테고리01 설명", "https://via.placeholder.com/150", "#FFFFFF")); + + mockMvc + .perform( + put(BASE_URL + "/{id}", 1L) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .content(content) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("카테고리명"), + fieldWithPath("description").type(JsonFieldType.STRING).description("카테고리 설명"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("이미지 URL"), + fieldWithPath("color").type(JsonFieldType.STRING).description("색상 코드") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("카테고리 ID"), + fieldWithPath("name").type(JsonFieldType.STRING).description("카테고리명"), + fieldWithPath("description").type(JsonFieldType.STRING).description("카테고리 설명"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("이미지 URL"), + fieldWithPath("color").type(JsonFieldType.STRING).description("색상 코드") + ) + ) + ); + } + + @Test + void deleteCategory() throws Exception { + mockMvc + .perform( + delete(BASE_URL + "/{id}", 1L) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + ) + .andExpect(status().isNoContent()) + .andDo( + restDocs.document() + ); + } +} \ No newline at end of file diff --git a/src/test/java/gift/web/controller/api/LoginControllerTest.java b/src/test/java/gift/web/controller/api/LoginControllerTest.java new file mode 100644 index 000000000..0e08217f7 --- /dev/null +++ b/src/test/java/gift/web/controller/api/LoginControllerTest.java @@ -0,0 +1,84 @@ +package gift.web.controller.api; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import gift.config.RestDocsConfiguration; +import gift.service.LoginService; +import gift.web.dto.response.LoginResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +@ActiveProfiles("test") +@SpringBootTest +@Import(RestDocsConfiguration.class) +@ExtendWith(RestDocumentationExtension.class) +class LoginControllerTest { + + private MockMvc mockMvc; + + @Autowired + protected RestDocumentationResultHandler restDocs; + + @Autowired + protected ObjectMapper objectMapper; + + @MockBean + private LoginService loginService; + + private static final String BASE_URL = "/api/login"; + + @BeforeEach + void setUp( + final RestDocumentationContextProvider provider + ) { + mockMvc = MockMvcBuilders + .standaloneSetup(new LoginController(loginService)) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) + .alwaysDo(restDocs) + .build(); + } + + + @Test + void kakaoLogin() throws Exception { + given(loginService.kakaoLogin(any())) + .willReturn(new LoginResponse("Bearer {{access_token}}")); + + mockMvc + .perform( + get(BASE_URL + "/oauth2/kakao") + .param("code", "kakao-auth-code") + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + queryParameters( + parameterWithName("code").optional().description("kakao-auth-code") + ), + responseFields( + fieldWithPath("accessToken").type(String.class).description("access-token(BEARER)") + ) + ) + ); + } +} \ No newline at end of file diff --git a/src/test/java/gift/web/controller/api/MemberApiControllerTest.java b/src/test/java/gift/web/controller/api/MemberApiControllerTest.java index 85204f1ea..3cd366a8d 100644 --- a/src/test/java/gift/web/controller/api/MemberApiControllerTest.java +++ b/src/test/java/gift/web/controller/api/MemberApiControllerTest.java @@ -1,20 +1,27 @@ package gift.web.controller.api; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertIterableEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import gift.authentication.token.Token; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import gift.authentication.token.JwtProvider; +import gift.config.RestDocsConfiguration; import gift.domain.Member; -import gift.repository.MemberRepository; +import gift.domain.Member.Builder; +import gift.domain.vo.Email; +import gift.mock.MockLoginMemberArgumentResolver; import gift.service.MemberService; import gift.service.WishProductService; -import gift.utils.CategoryDummyDataProvider; -import gift.utils.DatabaseCleanup; -import gift.utils.MemberDummyDataProvider; -import gift.utils.ProductDummyDataProvider; -import gift.utils.WishProductDummyDataProvider; import gift.web.dto.request.LoginRequest; import gift.web.dto.request.member.CreateMemberRequest; import gift.web.dto.request.wishproduct.UpdateWishProductRequest; @@ -22,205 +29,239 @@ import gift.web.dto.response.member.CreateMemberResponse; import gift.web.dto.response.member.ReadMemberResponse; import gift.web.dto.response.wishproduct.ReadAllWishProductsResponse; +import gift.web.dto.response.wishproduct.ReadWishProductResponse; import gift.web.dto.response.wishproduct.UpdateWishProductResponse; -import org.junit.jupiter.api.AfterEach; +import io.swagger.v3.core.util.Json; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.data.domain.PageRequest; -import org.springframework.http.HttpEntity; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; +import org.springframework.http.MediaType; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; @ActiveProfiles("test") -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SpringBootTest +@Import(RestDocsConfiguration.class) +@ExtendWith(RestDocumentationExtension.class) class MemberApiControllerTest { - @LocalServerPort - private int port; - - @Autowired - private TestRestTemplate restTemplate; - - @Autowired - private MemberDummyDataProvider memberDummyDataProvider; - - @Autowired - private ProductDummyDataProvider productDummyDataProvider; + private MockMvc mockMvc; @Autowired - private WishProductDummyDataProvider wishProductDummyDataProvider; + protected RestDocumentationResultHandler restDocs; @Autowired - private CategoryDummyDataProvider categoryDummyDataProvider; + protected ObjectMapper objectMapper; @Autowired - private DatabaseCleanup databaseCleanup; + private JwtProvider jwtProvider; - @Autowired + @MockBean private MemberService memberService; - @Autowired - private MemberRepository memberRepository; - - @Autowired + @MockBean private WishProductService wishProductService; - //테스트용 회원 - private Member member; - private Token token; - - @BeforeEach - void setUp() { - insertDummyData(100); - member = getTestMember(1L); - token = getAccessToken(); - } - - private Member getTestMember(Long id) { - return memberRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("ID: " + id +"인 회원이 존재하지 않습니다.")); - } - - private Token getAccessToken() { - LoginRequest loginRequest = new LoginRequest( - member.getEmail().getValue(), - member.getPassword().getValue() - ); - LoginResponse loginResponse = memberService.login(loginRequest); - return Token.from(loginResponse.getAccessToken()); - } + private String accessToken; - private void insertDummyData(int quantity) { - if (quantity < 2) { - throw new IllegalArgumentException("quantity는 2 이상이어야 합니다."); - } - memberDummyDataProvider.run(quantity); - productDummyDataProvider.run(quantity); - wishProductDummyDataProvider.run(quantity); - categoryDummyDataProvider.run(quantity); - } + private static final String BASE_URL = "/api/members"; - @AfterEach - void tearDown() { - databaseCleanup.execute(); + @BeforeEach + void setUp( + final RestDocumentationContextProvider provider + ) { + mockMvc = MockMvcBuilders + .standaloneSetup(new MemberApiController(memberService, wishProductService)) + .setCustomArgumentResolvers(new MockLoginMemberArgumentResolver(), new PageableHandlerMethodArgumentResolver()) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) + .alwaysDo(restDocs) + .build(); + + Member member = new Builder().id(1L).name("회원01").email(Email.from("member01@gmail.com")) + .build(); + accessToken = jwtProvider.generateToken(member).getValue(); } @Test - @DisplayName("회원 생성 요청에 대한 정상 응답") - void createMember() { - //given - CreateMemberRequest request = new CreateMemberRequest("test@gmail.com", "test1234", "test"); - String url = "http://localhost:" + port + "/api/members/register"; - - //when - ResponseEntity response = restTemplate.postForEntity(url, request, CreateMemberResponse.class); - - //then - Long newMemberId = response.getBody().getId(); - ReadMemberResponse findMember = memberService.readMember(newMemberId); - - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(newMemberId).isEqualTo(findMember.getId()), - () -> assertThat(request.getEmail()).isEqualTo(findMember.getEmail()), - () -> assertThat(request.getName()).isEqualTo(findMember.getName()), - () -> assertThat(request.getPassword()).isEqualTo(findMember.getPassword()) - ); + @DisplayName("회원 가입") + void createMember() throws Exception { + CreateMemberRequest request = new CreateMemberRequest("member01@gmail.com", "password01", + "member01"); + + String content = objectMapper.writeValueAsString(request); + + given(memberService.createMember(any())) + .willReturn(new CreateMemberResponse(1L, "member01@gmail.com", "member01")); + + mockMvc + .perform( + post(BASE_URL + "/register") + .contentType(MediaType.APPLICATION_JSON) + .content(content) + ) + .andExpect(status().isCreated()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"), + fieldWithPath("password").type(JsonFieldType.STRING).description("비밀번호"), + fieldWithPath("name").type(JsonFieldType.STRING).description("이름") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("회원 식별자"), + fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"), + fieldWithPath("name").type(JsonFieldType.STRING).description("이름") + ) + ) + ); } @Test - @DisplayName("로그인 요청에 대한 정상 응답") - void login() { - //given - String url = "http://localhost:" + port + "/api/members/login"; - String email = member.getEmail().getValue(); - String password = member.getPassword().getValue(); - LoginRequest request = new LoginRequest(email, password); - - //when - ResponseEntity response = restTemplate.postForEntity(url, request, LoginResponse.class); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().getAccessToken()).isNotNull() - ); + @DisplayName("로그인") + void login() throws Exception { + LoginRequest request = new LoginRequest("member01@gmail.com", "password01"); + String content = objectMapper.writeValueAsString(request); + + given(memberService.login(any())) + .willReturn(new LoginResponse(accessToken)); + + mockMvc + .perform( + post(BASE_URL + "/login") + .contentType(MediaType.APPLICATION_JSON) + .content(content) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"), + fieldWithPath("password").type(JsonFieldType.STRING).description("비밀번호") + ), + responseFields( + fieldWithPath("accessToken").type(JsonFieldType.STRING).description("Bearer Token") + ) + ) + ); } @Test - @DisplayName("위시 리스트 조회 요청에 대한 정상 응답") - void readWishProduct() { - //given - String url = "http://localhost:" + port + "/api/members/wishlist"; - HttpHeaders httpHeaders = getHttpHeaders(); - HttpEntity httpEntity = new HttpEntity(httpHeaders); - - PageRequest defaultPageRequest = PageRequest.of(0, 10); - ReadAllWishProductsResponse expectedWishProducts = wishProductService.readAllWishProducts(member.getId(), - defaultPageRequest); - - //when - ResponseEntity response = restTemplate.exchange(url, - HttpMethod.GET, httpEntity, ReadAllWishProductsResponse.class); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertIterableEquals(response.getBody().getWishlist(), expectedWishProducts.getWishlist()) - ); + @DisplayName("회원 조회") + void readMember() throws Exception { + given(memberService.readMember(any(Long.class))) + .willReturn(new ReadMemberResponse(1L, "member01@gmail.com", "password01", "member01")); + + mockMvc + .perform( + get(BASE_URL + "/{memberId}", 1L) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("회원 식별자"), + fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"), + fieldWithPath("password").type(JsonFieldType.STRING).description("비밀번호"), + fieldWithPath("name").type(JsonFieldType.STRING).description("이름") + ) + ) + ); } @Test - @DisplayName("위시 리스트 상품 수정 요청에 대한 정상 응답") - void updateWishProduct() { - //given - Long wishProductId = 1L; - String url = "http://localhost:" + port + "/api/members/wishlist/" + wishProductId; - HttpHeaders httpHeaders = getHttpHeaders(); - - UpdateWishProductRequest request = new UpdateWishProductRequest(3); - - HttpEntity httpEntity = new HttpEntity(request, httpHeaders); - - //when - ResponseEntity response = restTemplate.exchange(url, - HttpMethod.PUT, httpEntity, UpdateWishProductResponse.class); - - //then - assertAll( - () -> assertTrue(response.getStatusCode().is2xxSuccessful()), - () -> assertThat(response.getBody().getQuantity()).isEqualTo(request.getQuantity()) + @DisplayName("위시 상품 조회") + void readWishProduct() throws Exception { + ReadAllWishProductsResponse response = new ReadAllWishProductsResponse( + List.of(new ReadWishProductResponse(1L, 1L, "product01", 1000, 5, "https://via.placeholder.com/150")) ); + + given(wishProductService.readAllWishProducts(any(Long.class), any())) + .willReturn(response); + + mockMvc + .perform( + get(BASE_URL + "/wishlist") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + queryParameters( + parameterWithName("page").optional().description("페이지 번호"), + parameterWithName("size").optional().description("페이지 크기") + ), + responseFields( + fieldWithPath("wishlist[].id").type(JsonFieldType.NUMBER).description("위시 상품 식별자"), + fieldWithPath("wishlist[].productId").type(JsonFieldType.NUMBER).description("상품 식별자"), + fieldWithPath("wishlist[].name").type(JsonFieldType.STRING).description("상품명"), + fieldWithPath("wishlist[].price").type(JsonFieldType.NUMBER).description("가격"), + fieldWithPath("wishlist[].quantity").type(JsonFieldType.NUMBER).description("재고 수량"), + fieldWithPath("wishlist[].imageUrl").type(JsonFieldType.STRING).description("이미지 URL") + ) + ) + ); } @Test - @DisplayName("위시 리스트 상품 삭제 요청에 대한 정상 응답") - void deleteWishProduct() { - //given - Long wishProductId = 2L; - String url = "http://localhost:" + port + "/api/members/wishlist/" + wishProductId; - HttpHeaders httpHeaders = getHttpHeaders(); - HttpEntity httpEntity = new HttpEntity(httpHeaders); - - //when - ResponseEntity response = restTemplate.exchange(url, HttpMethod.DELETE, httpEntity, - Void.class); - - //then - assertTrue(response.getStatusCode().is2xxSuccessful()); + @DisplayName("위시 상품 수정") + void updateWishProduct() throws Exception { + UpdateWishProductRequest request = new UpdateWishProductRequest(5); + String content = Json.mapper().writeValueAsString(request); + + given(wishProductService.updateWishProduct(any(Long.class), any())) + .willReturn(new UpdateWishProductResponse(1L, 1L, "product01", 1000, 5, "https://via.placeholder.com/150")); + + mockMvc + .perform( + put(BASE_URL + "/wishlist/{wishProductId}", 1L) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("quantity").type(JsonFieldType.NUMBER).description("수량") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("위시 상품 식별자"), + fieldWithPath("productId").type(JsonFieldType.NUMBER).description("상품 식별자"), + fieldWithPath("name").type(JsonFieldType.STRING).description("상품명"), + fieldWithPath("price").type(JsonFieldType.NUMBER).description("가격"), + fieldWithPath("quantity").type(JsonFieldType.NUMBER).description("재고 수량"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("이미지 URL") + ) + ) + ); } - private HttpHeaders getHttpHeaders() { - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setBearerAuth(token.getValue()); - return httpHeaders; + @Test + @DisplayName("위시 상품 삭제") + void deleteWishProduct() throws Exception { + mockMvc + .perform( + delete(BASE_URL + "/wishlist/{wishProductId}", 1L) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + ) + .andExpect(status().isNoContent()) + .andDo( + restDocs.document() + ); } } \ No newline at end of file diff --git a/src/test/java/gift/web/controller/api/ProductApiControllerTest.java b/src/test/java/gift/web/controller/api/ProductApiControllerTest.java new file mode 100644 index 000000000..24d5347a3 --- /dev/null +++ b/src/test/java/gift/web/controller/api/ProductApiControllerTest.java @@ -0,0 +1,524 @@ +package gift.web.controller.api; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import gift.authentication.token.JwtProvider; +import gift.config.RestDocsConfiguration; +import gift.converter.StringToUrlConverter; +import gift.domain.Category; +import gift.domain.Member; +import gift.domain.Member.Builder; +import gift.domain.Product; +import gift.domain.ProductOption; +import gift.domain.vo.Color; +import gift.domain.vo.Email; +import gift.mock.MockLoginMemberArgumentResolver; +import gift.service.OrderService; +import gift.service.ProductOptionService; +import gift.service.ProductService; +import gift.service.WishProductService; +import gift.web.dto.request.order.CreateOrderRequest; +import gift.web.dto.request.product.CreateProductRequest; +import gift.web.dto.request.product.UpdateProductRequest; +import gift.web.dto.request.productoption.CreateProductOptionRequest; +import gift.web.dto.request.productoption.UpdateProductOptionRequest; +import gift.web.dto.request.wishproduct.CreateWishProductRequest; +import gift.web.dto.response.category.ReadCategoryResponse; +import gift.web.dto.response.order.OrderResponse; +import gift.web.dto.response.product.CreateProductResponse; +import gift.web.dto.response.product.ReadAllProductsResponse; +import gift.web.dto.response.product.ReadProductResponse; +import gift.web.dto.response.product.UpdateProductResponse; +import gift.web.dto.response.productoption.CreateProductOptionResponse; +import gift.web.dto.response.productoption.ReadAllProductOptionsResponse; +import gift.web.dto.response.productoption.ReadProductOptionResponse; +import gift.web.dto.response.productoption.UpdateProductOptionResponse; +import gift.web.dto.response.wishproduct.CreateWishProductResponse; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +@ActiveProfiles("test") +@SpringBootTest +@Import(RestDocsConfiguration.class) +@ExtendWith(RestDocumentationExtension.class) +class ProductApiControllerTest { + + private MockMvc mockMvc; + + @Autowired + protected RestDocumentationResultHandler restDocs; + + @Autowired + protected ObjectMapper objectMapper; + + @Autowired + private JwtProvider jwtProvider; + + @MockBean + private ProductService productService; + @MockBean + private WishProductService wishProductService; + @MockBean + private ProductOptionService productOptionService; + @MockBean + private OrderService orderService; + + private String accessToken; + + private static final String BASE_URL = "/api/products"; + + @BeforeEach + void setUp( + final RestDocumentationContextProvider provider + ) { + mockMvc = MockMvcBuilders + .standaloneSetup(new ProductApiController(productService, wishProductService, productOptionService, orderService)) + .setCustomArgumentResolvers(new MockLoginMemberArgumentResolver(), new PageableHandlerMethodArgumentResolver()) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) + .alwaysDo(restDocs) + .build(); + + Member member = new Builder().id(1L).name("회원01").email(Email.from("member01@gmail.com")) + .build(); + accessToken = jwtProvider.generateToken(member).getValue(); + } + + @Test + @DisplayName("전체 상품 조회") + void readAllProducts() throws Exception { + given(productService.readAllProducts(any(Pageable.class))) + .willReturn(ReadAllProductsResponse.from( + List.of(new ReadProductResponse(1L, "상품01", 1000, "https://via.placeholder.com/150", + List.of(new ReadProductOptionResponse(1L, "상품 옵션 01", 100)), + new ReadCategoryResponse(1L, "카테고리01", "카테고리01 입니다", "https://via.placeholder.com/150", "#FFFFFF"))) + )); + + mockMvc + .perform( + get(BASE_URL) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .param("page", "0") + .param("size", "10") + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + queryParameters( + parameterWithName("page").optional().description("페이지 번호"), + parameterWithName("size").optional().description("페이지 크기") + ), + responseFields( + fieldWithPath("products[].id").type(JsonFieldType.NUMBER).description("상품 ID"), + fieldWithPath("products[].name").type(JsonFieldType.STRING).description("상품명"), + fieldWithPath("products[].price").type(JsonFieldType.NUMBER).description("상품 가격"), + fieldWithPath("products[].imageUrl").type(JsonFieldType.STRING).description("상품 이미지 URL"), + fieldWithPath("products[].options").type(JsonFieldType.ARRAY).description("상품 옵션 목록"), + fieldWithPath("products[].options[].id").type(JsonFieldType.NUMBER).description("상품 옵션 ID"), + fieldWithPath("products[].options[].name").type(JsonFieldType.STRING).description("상품 옵션명"), + fieldWithPath("products[].options[].stock").type(JsonFieldType.NUMBER).description("상품 옵션 재고"), + fieldWithPath("products[].category.id").type(JsonFieldType.NUMBER).description("카테고리 ID"), + fieldWithPath("products[].category.name").type(JsonFieldType.STRING).description("카테고리명"), + fieldWithPath("products[].category.description").type(JsonFieldType.STRING).description("카테고리 설명"), + fieldWithPath("products[].category.imageUrl").type(JsonFieldType.STRING).description("카테고리 이미지 URL"), + fieldWithPath("products[].category.color").type(JsonFieldType.STRING).description("카테고리 색상") + ) + ) + ); + } + +// todo requestParam 만 다른 경로에 대해 테스트 코드 동작시키기 +// @Test +// @DisplayName("카테고리로 상품 조회") +// void readProductsByCategoryId() throws Exception { +// given(productService.readProductsByCategoryId(any(Long.class), any(Pageable.class))) +// .willReturn(ReadAllProductsResponse.from( +// List.of(new ReadProductResponse(1L, "상품01", 1000, "https://via.placeholder.com/150", +// List.of(new ReadProductOptionResponse(1L, "상품 옵션 01", 100)), +// new ReadCategoryResponse(1L, "카테고리01", "카테고리01 입니다", "https://via.placeholder.com/150", "#FFFFFF"))) +// )); +// +// mockMvc +// .perform( +// get(BASE_URL) +// .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) +// ) +// .andExpect(status().isOk()) +// .andDo( +// restDocs.document( +// queryParameters( +// parameterWithName("page").optional().description("페이지 번호"), +// parameterWithName("size").optional().description("페이지 크기"), +// parameterWithName("categoryId").description("카테고리 아이디") +// ), +// responseFields( +// fieldWithPath("products[].id").type(JsonFieldType.NUMBER).description("상품 ID"), +// fieldWithPath("products[].name").type(JsonFieldType.STRING).description("상품명"), +// fieldWithPath("products[].price").type(JsonFieldType.NUMBER).description("상품 가격"), +// fieldWithPath("products[].imageUrl").type(JsonFieldType.STRING).description("상품 이미지 URL"), +// fieldWithPath("products[].productOptions").type(JsonFieldType.ARRAY).description("상품 옵션 목록"), +// fieldWithPath("products[].productOptions[].id").type(JsonFieldType.NUMBER).description("상품 옵션 ID"), +// fieldWithPath("products[].productOptions[].name").type(JsonFieldType.STRING).description("상품 옵션명"), +// fieldWithPath("products[].productOptions[].stock").type(JsonFieldType.NUMBER).description("상품 옵션 재고"), +// fieldWithPath("products[].category.id").type(JsonFieldType.NUMBER).description("카테고리 ID"), +// fieldWithPath("products[].category.name").type(JsonFieldType.STRING).description("카테고리명"), +// fieldWithPath("products[].category.description").type(JsonFieldType.STRING).description("카테고리 설명"), +// fieldWithPath("products[].category.imageUrl").type(JsonFieldType.STRING).description("카테고리 이미지 URL"), +// fieldWithPath("products[].category.color").type(JsonFieldType.STRING).description("카테고리 색상") +// ) +// ) +// ); +// } + + @Test + @DisplayName("상품 생성") + void createProduct() throws Exception { + CreateProductRequest request = new CreateProductRequest("상품01", 1000, + "https://via.placeholder.com/150", 1L, + List.of(new CreateProductOptionRequest("상품 옵션 01", 100))); + String content = objectMapper.writeValueAsString(request); + + given(productService.createProduct(any(CreateProductRequest.class))) + .willReturn(new CreateProductResponse(1L, "상품01", 1000, "https://via.placeholder.com/150", List.of(new ProductOption.Builder().id(1L).productId(1L).name("상품 옵션 01").stock(100).build())) ); + + mockMvc + .perform( + post(BASE_URL) + .content(content) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + ) + .andExpect(status().isCreated()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("상품명"), + fieldWithPath("price").type(JsonFieldType.NUMBER).description("상품 가격"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("상품 이미지 URL"), + fieldWithPath("categoryId").type(JsonFieldType.NUMBER).description("카테고리 ID"), + fieldWithPath("productOptions").type(JsonFieldType.ARRAY).description("상품 옵션 목록"), + fieldWithPath("productOptions[].name").type(JsonFieldType.STRING).description("상품 옵션명"), + fieldWithPath("productOptions[].stock").type(JsonFieldType.NUMBER).description("상품 옵션 재고") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("상품 ID"), + fieldWithPath("name").type(JsonFieldType.STRING).description("상품명"), + fieldWithPath("price").type(JsonFieldType.NUMBER).description("상품 가격"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("상품 이미지 URL"), + fieldWithPath("options").type(JsonFieldType.ARRAY).description("상품 옵션 목록"), + fieldWithPath("options[].id").type(JsonFieldType.NUMBER).description("상품 옵션 ID"), + fieldWithPath("options[].name").type(JsonFieldType.STRING).description("상품 옵션명"), + fieldWithPath("options[].stock").type(JsonFieldType.NUMBER).description("상품 옵션 재고"), + fieldWithPath("options[].productId").type(JsonFieldType.NUMBER).description("상품 옵션 상품 ID") + ) + ) + ); + } + + @Test + @DisplayName("단일 상품 조회") + void readProduct() throws Exception { + given(productService.readProductById(any(Long.class))) + .willReturn(new ReadProductResponse(1L, "상품01", 1000, "https://via.placeholder.com/150", + List.of(new ReadProductOptionResponse(1L, "상품 옵션 01", 100)), + new ReadCategoryResponse(1L, "카테고리01", "카테고리01 입니다", "https://via.placeholder.com/150", "#FFFFFF"))); + + mockMvc + .perform( + get(BASE_URL + "/{productId}", 1L) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("상품 ID"), + fieldWithPath("name").type(JsonFieldType.STRING).description("상품명"), + fieldWithPath("price").type(JsonFieldType.NUMBER).description("상품 가격"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("상품 이미지 URL"), + fieldWithPath("options").type(JsonFieldType.ARRAY).description("상품 옵션 목록"), + fieldWithPath("options[].id").type(JsonFieldType.NUMBER).description("상품 옵션 ID"), + fieldWithPath("options[].name").type(JsonFieldType.STRING).description("상품 옵션명"), + fieldWithPath("options[].stock").type(JsonFieldType.NUMBER).description("상품 옵션 재고"), + fieldWithPath("category.id").type(JsonFieldType.NUMBER).description("카테고리 ID"), + fieldWithPath("category.name").type(JsonFieldType.STRING).description("카테고리명"), + fieldWithPath("category.description").type(JsonFieldType.STRING).description("카테고리 설명"), + fieldWithPath("category.imageUrl").type(JsonFieldType.STRING).description("카테고리 이미지 URL"), + fieldWithPath("category.color").type(JsonFieldType.STRING).description("카테고리 색상") + ) + ) + ); + } + + @Test + @DisplayName("상품 수정") + void updateProduct() throws Exception { + UpdateProductRequest request = new UpdateProductRequest("상품01", 1000, "https://via.placeholder.com/150"); + + String content = objectMapper.writeValueAsString(request); + + given(productService.updateProduct(any(Long.class), any())) + .willReturn(UpdateProductResponse.from(new Product.Builder() + .id(1L) + .name("상품01") + .price(1000) + .productOptions( + List.of( + new ProductOption.Builder() + .id(1L) + .productId(1L) + .name("상품 옵션 01") + .stock(100) + .build() + )) + .imageUrl(StringToUrlConverter.convert("https://via.placeholder.com/150")) + .category(new Category.Builder() + .id(1L) + .name("카테고리01") + .color(Color.from("#FFFFFF")) + .description("카테고리01 입니다") + .imageUrl(StringToUrlConverter.convert("https://via.placeholder.com/150")) + .build()) + .build())); + + mockMvc + .perform( + put(BASE_URL + "/{productId}", 1L) + .content(content) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("상품명"), + fieldWithPath("price").type(JsonFieldType.NUMBER).description("상품 가격"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("상품 이미지 URL") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("상품 ID"), + fieldWithPath("name").type(JsonFieldType.STRING).description("상품명"), + fieldWithPath("price").type(JsonFieldType.NUMBER).description("상품 가격"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("상품 이미지 URL"), + fieldWithPath("category.id").type(JsonFieldType.NUMBER).description("카테고리 ID"), + fieldWithPath("category.name").type(JsonFieldType.STRING).description("카테고리명"), + fieldWithPath("category.description").type(JsonFieldType.STRING).description("카테고리 설명"), + fieldWithPath("category.imageUrl").type(JsonFieldType.STRING).description("카테고리 이미지 URL"), + fieldWithPath("category.color").type(JsonFieldType.STRING).description("카테고리 색상") + ) + ) + ); + } + + @Test + @DisplayName("상품 삭제") + void deleteProduct() throws Exception { + mockMvc + .perform( + delete(BASE_URL + "/{productId}", 1L) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + ) + .andExpect(status().isNoContent()) + .andDo( + restDocs.document() + ); + } + + @Test + @DisplayName("위시 상품 추가") + void createWishProduct() throws Exception { + CreateWishProductRequest request = new CreateWishProductRequest(1L, 1); + String content = objectMapper.writeValueAsString(request); + + given(wishProductService.createWishProduct(any(Long.class), any())) + .willReturn(new CreateWishProductResponse(1L, 1)); + + mockMvc + .perform( + post(BASE_URL + "/wish") + .content(content) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("productId").type(JsonFieldType.NUMBER).description("상품 ID"), + fieldWithPath("quantity").type(JsonFieldType.NUMBER).description("수량") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("위시 상품 ID"), + fieldWithPath("quantity").type(JsonFieldType.NUMBER).description("수량") + ) + ) + ); + } + + @Test + @DisplayName("상품 옵션 조회") + void readOptions() throws Exception { + given(productOptionService.readAllOptions(any(Long.class))) + .willReturn(ReadAllProductOptionsResponse.from( + List.of(new ReadProductOptionResponse(1L, "상품 옵션 01", 100)) + )); + + mockMvc + .perform( + get(BASE_URL + "/{productId}/options", 1L) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + responseFields( + fieldWithPath("options[].id").type(JsonFieldType.NUMBER).description("상품 옵션 ID"), + fieldWithPath("options[].name").type(JsonFieldType.STRING).description("상품 옵션명"), + fieldWithPath("options[].stock").type(JsonFieldType.NUMBER).description("상품 옵션 재고") + ) + ) + ); + } + + @Test + @DisplayName("상품 옵션 생성") + void createOption() throws Exception { + CreateProductOptionRequest request = new CreateProductOptionRequest( + "상품 옵션 01", 100); + String content = objectMapper.writeValueAsString(request); + + given(productOptionService.createOption(any(Long.class), any())) + .willReturn(new CreateProductOptionResponse(1L, "상품 옵션 01", 100)); + + mockMvc + .perform( + post(BASE_URL + "/{productId}/options", 1L) + .content(content) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + ) + .andExpect(status().isCreated()) + .andDo( + restDocs.document( + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("상품 옵션 ID"), + fieldWithPath("name").type(JsonFieldType.STRING).description("상품 옵션명"), + fieldWithPath("stock").type(JsonFieldType.NUMBER).description("상품 옵션 재고") + ) + ) + ); + } + + @Test + @DisplayName("상품 주문") + void orderProduct() throws Exception { + + CreateOrderRequest request = new CreateOrderRequest(1L, 1, "message"); + String content = objectMapper.writeValueAsString(request); + + given(orderService.createOrder(any(String.class), any(Long.class), any(Long.class), any())) + .willReturn(new OrderResponse(1L, 1L, 10, "message")); + + mockMvc + .perform( + post(BASE_URL + "/{productId}/order", 1L) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .content(content) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("optionId").type(JsonFieldType.NUMBER).description("상품 옵션 ID"), + fieldWithPath("quantity").type(JsonFieldType.NUMBER).description("주문 수량"), + fieldWithPath("message").type(JsonFieldType.STRING).description("메시지") + ), + responseFields( + fieldWithPath("productId").type(JsonFieldType.NUMBER).description("상품 ID"), + fieldWithPath("optionId").type(JsonFieldType.NUMBER).description("상품 옵션 ID"), + fieldWithPath("quantity").type(JsonFieldType.NUMBER).description("주문 수량"), + fieldWithPath("message").type(JsonFieldType.STRING).description("메시지") + ) + ) + ); + } + + @Test + @DisplayName("상품 옵션 수정") + void updateOption() throws Exception { + UpdateProductOptionRequest request = new UpdateProductOptionRequest( + "상품 옵션 01", 100); + String content = objectMapper.writeValueAsString(request); + + given(productOptionService.updateOption(any(Long.class), any(Long.class), any())) + .willReturn(new UpdateProductOptionResponse(1L, "상품 옵션 01", 100)); + + mockMvc + .perform( + put(BASE_URL + "/{productId}/options/{optionId}", 1L, 1L) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .content(content) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("상품 옵션명"), + fieldWithPath("stock").type(JsonFieldType.NUMBER).description("상품 옵션 재고") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("상품 옵션 ID"), + fieldWithPath("name").type(JsonFieldType.STRING).description("상품 옵션명"), + fieldWithPath("stock").type(JsonFieldType.NUMBER).description("상품 옵션 재고") + ) + ) + ); + } + + @Test + @DisplayName("상품 옵션 삭제") + void deleteOption() throws Exception { + mockMvc + .perform( + delete(BASE_URL + "/{productId}/options/{optionId}", 1L, 1L) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + ) + .andExpect(status().isNoContent()) + .andDo( + restDocs.document() + ); + } +} \ No newline at end of file