diff --git a/order-service/src/docs/asciidoc/api-docs.adoc b/order-service/src/docs/asciidoc/api-docs.adoc new file mode 100644 index 0000000..f38345e --- /dev/null +++ b/order-service/src/docs/asciidoc/api-docs.adoc @@ -0,0 +1,74 @@ +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 2 +:sectlinks: + + +[[overview]] += 개요 + +[[overview-http-verbs]] +== HTTP 동사 + +본 REST API에서 사용하는 HTTP 동사(verbs)는 가능한한 표준 HTTP와 REST 규약을 따릅니다. + +|=== +| 동사 | 용례 + +| `GET` +| 리소스를 가져올 때 사용 + +| `POST` +| 새 리소스를 만들 때 사용 + +| `PUT` +| 기존 리소스를 수정할 때 사용 + +| `PATCH` +| 기존 리소스의 일부를 수정할 때 사용 + +| `DELETE` +| 기존 리소스를 삭제할 떄 사용 +|=== + +[[overview-http-status-codes]] +== HTTP 상태 코드 + +본 REST API에서 사용하는 HTTP 상태 코드는 가능한 표준 HTTP와 REST 규약을 따릅니다. + +|=== +| 상태 코드 | 용례 + +| `200 OK` +| 요청을 성공적으로 처리함 + +| `201 Created` +| 새 리소스를 성공적으로 생성함. 응답의 `Location` 헤더에 해당 리소스의 URI가 담겨있다. + +| `204 No Content` +| 기존 리소스를 성공적으로 수정함. + +| `400 Bad Request` +| 잘못된 요청을 보낸 경우. 응답 본문에 더 오류에 대한 정보가 담겨있다. + +| `404 Not Found` +| 요청한 리소스가 없음. + +| `409 Conflict` +| 클라이언트의 요청이 서버의 상태와 충돌이 발생한 경우. +|=== + +[[snippets-write-convention]] +== snippets 작성 컨벤션 +domain-httpRequestCode-etc + +== 주문 +=== 점주 서비스 - 주문 페이지 +- 페이지 offset : 6 + +operation::orderMain-get[snippets='curl-request,http-request,http-response,request-parameters,response-fields'] +=== 점주 서비스 - 주문 페이지 (잘못된 파라미터 형식) + +operation::orderMain-get-badParameterException[snippets='curl-request,http-request,http-response,request-parameters,response-fields'] diff --git a/order-service/src/main/java/com/justpickup/orderservice/domain/order/dto/OrderSearchCondition.java b/order-service/src/main/java/com/justpickup/orderservice/domain/order/dto/OrderSearchCondition.java new file mode 100644 index 0000000..5324e0b --- /dev/null +++ b/order-service/src/main/java/com/justpickup/orderservice/domain/order/dto/OrderSearchCondition.java @@ -0,0 +1,31 @@ +package com.justpickup.orderservice.domain.order.dto; + +import lombok.*; + +import javax.validation.constraints.Min; +import javax.validation.constraints.Pattern; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +@Data @NoArgsConstructor @AllArgsConstructor +public class OrderSearchCondition { + + @Pattern(regexp = "^(19|20)\\d{2}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[0-1])$", + message = "YYYY-MM-DD 형식에 맞게 작성되지 않았습니다.") + private String orderDate; + @Min(value = 1, + message = "마지막 주문 번호는 1보다 작을 수 없습니다.") + private Long lastOrderId; + + public LocalDateTime getOrderStartTime() { + LocalDate orderTime = LocalDate.parse(orderDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")); + return orderTime.atStartOfDay(); + } + + public LocalDateTime getOrderEndTime() { + LocalDate orderTime = LocalDate.parse(orderDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")); + return LocalDateTime.of(orderTime, LocalTime.of(23, 59, 59)); + } +} \ No newline at end of file diff --git a/order-service/src/main/java/com/justpickup/orderservice/domain/order/entity/Order.java b/order-service/src/main/java/com/justpickup/orderservice/domain/order/entity/Order.java index 4d9547b..7b2554e 100644 --- a/order-service/src/main/java/com/justpickup/orderservice/domain/order/entity/Order.java +++ b/order-service/src/main/java/com/justpickup/orderservice/domain/order/entity/Order.java @@ -1,18 +1,16 @@ package com.justpickup.orderservice.domain.order.entity; -import com.justpickup.orderservice.domain.order.dto.OrderDto; -import com.justpickup.orderservice.domain.orderItem.dto.OrderItemDto; import com.justpickup.orderservice.domain.orderItem.entity.OrderItem; import com.justpickup.orderservice.domain.transaction.entity.Transaction; import com.justpickup.orderservice.global.entity.BaseEntity; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; import javax.persistence.*; import java.time.LocalDateTime; import java.util.List; -import java.util.stream.Collectors; @Entity @Table(name = "orders") @@ -28,6 +26,8 @@ public class Order extends BaseEntity { private Long userCouponId; + private Long storeId; + private Long orderPrice; private LocalDateTime orderTime; @@ -40,6 +40,7 @@ public class Order extends BaseEntity { @OneToOne(mappedBy = "order", fetch = FetchType.LAZY) private Transaction transaction; + @BatchSize(size = 100) @OneToMany(mappedBy = "order") private List orderItems; diff --git a/order-service/src/main/java/com/justpickup/orderservice/domain/order/repository/OrderRepositoryCustom.java b/order-service/src/main/java/com/justpickup/orderservice/domain/order/repository/OrderRepositoryCustom.java index de4b027..7b178e7 100644 --- a/order-service/src/main/java/com/justpickup/orderservice/domain/order/repository/OrderRepositoryCustom.java +++ b/order-service/src/main/java/com/justpickup/orderservice/domain/order/repository/OrderRepositoryCustom.java @@ -1,13 +1,13 @@ package com.justpickup.orderservice.domain.order.repository; +import com.justpickup.orderservice.domain.order.dto.OrderSearchCondition; import com.justpickup.orderservice.domain.order.entity.Order; +import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; -import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.LocalTime; import java.util.List; import static com.justpickup.orderservice.domain.order.entity.QOrder.order; @@ -18,18 +18,34 @@ public class OrderRepositoryCustom { private final JPAQueryFactory queryFactory; - public List findOrderMainBetweenOrderDate(LocalDate orderDate) { - LocalDateTime start = orderDate.atStartOfDay(); - LocalDateTime end = LocalDateTime.of(orderDate, LocalTime.of(23, 59, 59)); + /* + SELECT * + FROM order + WHERE 조건문 + AND id < 마지막 조회 ID + ORDER BY id desc + LIMIT 페이지 사이즈 + */ + public List findOrderMain(OrderSearchCondition condition, Long storeId) { + LocalDateTime start = condition.getOrderStartTime(); + LocalDateTime end = condition.getOrderEndTime(); return queryFactory .selectFrom(order) - .join(order.orderItems).fetchJoin() .join(order.transaction).fetchJoin() .where( - order.orderTime.between(start, end) + order.orderTime.between(start, end), + order.storeId.eq(storeId), + orderIdLt(condition.getLastOrderId()) ) + .orderBy(order.orderTime.desc()) + .limit(6) .distinct() .fetch(); } + + private BooleanExpression orderIdLt(Long lastOrderId) { + return lastOrderId != null ? order.id.lt(lastOrderId) : null; + } + } diff --git a/order-service/src/main/java/com/justpickup/orderservice/domain/order/service/OrderService.java b/order-service/src/main/java/com/justpickup/orderservice/domain/order/service/OrderService.java index 1ef99e9..6c9e6b8 100644 --- a/order-service/src/main/java/com/justpickup/orderservice/domain/order/service/OrderService.java +++ b/order-service/src/main/java/com/justpickup/orderservice/domain/order/service/OrderService.java @@ -1,10 +1,10 @@ package com.justpickup.orderservice.domain.order.service; import com.justpickup.orderservice.domain.order.dto.OrderDto; +import com.justpickup.orderservice.domain.order.dto.OrderSearchCondition; -import java.time.LocalDate; import java.util.List; public interface OrderService { - List findOrderMain(LocalDate localDate); + List findOrderMain(OrderSearchCondition condition, Long storeId); } diff --git a/order-service/src/main/java/com/justpickup/orderservice/domain/order/service/OrderServiceImpl.java b/order-service/src/main/java/com/justpickup/orderservice/domain/order/service/OrderServiceImpl.java index 2626f8f..87c0db6 100644 --- a/order-service/src/main/java/com/justpickup/orderservice/domain/order/service/OrderServiceImpl.java +++ b/order-service/src/main/java/com/justpickup/orderservice/domain/order/service/OrderServiceImpl.java @@ -1,18 +1,18 @@ package com.justpickup.orderservice.domain.order.service; import com.justpickup.orderservice.domain.order.dto.OrderDto; +import com.justpickup.orderservice.domain.order.dto.OrderSearchCondition; import com.justpickup.orderservice.domain.order.repository.OrderRepository; import com.justpickup.orderservice.domain.order.repository.OrderRepositoryCustom; import com.justpickup.orderservice.global.client.store.GetItemResponse; import com.justpickup.orderservice.global.client.store.StoreClient; -import com.justpickup.orderservice.global.client.user.UserClient; import com.justpickup.orderservice.global.client.user.GetCustomerResponse; +import com.justpickup.orderservice.global.client.user.UserClient; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDate; import java.util.List; import java.util.stream.Collectors; @@ -28,23 +28,24 @@ public class OrderServiceImpl implements OrderService { private final UserClient userClient; @Override - public List findOrderMain(LocalDate orderDate) { + public List findOrderMain(OrderSearchCondition condition, Long storeId) { // 주문 가져오기 - List orderDtoList = orderRepositoryCustom.findOrderMainBetweenOrderDate(orderDate) - .stream() - .map(OrderDto::createFullField) - .collect(Collectors.toList()); + List orderDtoList = + orderRepositoryCustom.findOrderMain(condition, storeId) + .stream() + .map(OrderDto::createFullField) + .collect(Collectors.toList()); // 사용자명 및 아이템 이름 가져오기 orderDtoList.forEach(orderDto -> { - GetCustomerResponse getCustomerResponse = userClient.getUser(orderDto.getUserId()) - .getData(); + GetCustomerResponse getCustomerResponse = + userClient.getUser(orderDto.getUserId()).getData(); orderDto.setUserName(getCustomerResponse.getUserName()); orderDto.getOrderItemDtoList() .forEach(orderItemDto -> { - GetItemResponse getItemResponse = storeClient.getItem(orderItemDto.getItemId()) - .getData(); + GetItemResponse getItemResponse = + storeClient.getItem(orderItemDto.getItemId()).getData(); orderItemDto.setItemName(getItemResponse.getName()); }); }); diff --git a/order-service/src/main/java/com/justpickup/orderservice/domain/order/web/OrderController.java b/order-service/src/main/java/com/justpickup/orderservice/domain/order/web/OrderController.java index 91ca880..c55c363 100644 --- a/order-service/src/main/java/com/justpickup/orderservice/domain/order/web/OrderController.java +++ b/order-service/src/main/java/com/justpickup/orderservice/domain/order/web/OrderController.java @@ -1,6 +1,7 @@ package com.justpickup.orderservice.domain.order.web; import com.justpickup.orderservice.domain.order.dto.OrderDto; +import com.justpickup.orderservice.domain.order.dto.OrderSearchCondition; import com.justpickup.orderservice.domain.order.entity.OrderStatus; import com.justpickup.orderservice.domain.order.service.OrderService; import com.justpickup.orderservice.domain.orderItem.dto.OrderItemDto; @@ -16,8 +17,6 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; -import javax.validation.constraints.Pattern; -import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.stream.Collectors; @@ -30,30 +29,21 @@ public class OrderController { private final OrderService orderService; @GetMapping("/orderMain") - public ResponseEntity orderMain(@Valid OrderMainRequest orderMainRequest) { + public ResponseEntity orderMain(@Valid OrderSearchCondition condition) { + // TODO: 2022/02/04 JWT 구현 시 변경 요망 + Long userId = 1L; + Long storeId = 1L; - List orderDto = orderService.findOrderMain(orderMainRequest.convertOrderTimeToLocalDate()); + List orderDto = orderService.findOrderMain(condition, storeId); List orderMainResponses = orderDto.stream() .map(OrderMainResponse::new) .collect(Collectors.toList()); - + return ResponseEntity.status(HttpStatus.OK) .body(Result.createSuccessResult(orderMainResponses)); } - @Data @NoArgsConstructor @AllArgsConstructor - static class OrderMainRequest { - // yyyy-mm-dd 형태를 가지는 패턴 조사 - @Pattern(regexp = "^(19|20)\\d{2}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[0-1])$", - message = "YYYY-MM-DD 형식에 맞게 작성되지 않았습니다.") - private String orderTime; - - public LocalDate convertOrderTimeToLocalDate() { - return LocalDate.parse(orderTime, DateTimeFormatter.ofPattern("yyyy-MM-dd")); - } - } - @Data @NoArgsConstructor @AllArgsConstructor static class OrderMainResponse { private Long orderId; diff --git a/order-service/src/main/resources/static/docs/api-docs.html b/order-service/src/main/resources/static/docs/api-docs.html new file mode 100644 index 0000000..148f1af --- /dev/null +++ b/order-service/src/main/resources/static/docs/api-docs.html @@ -0,0 +1,836 @@ + + + + + + + +개요 + + + + + + +
+
+

HTTP 동사

+
+
+

본 REST API에서 사용하는 HTTP 동사(verbs)는 가능한한 표준 HTTP와 REST 규약을 따릅니다.

+
+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
동사용례

GET

리소스를 가져올 때 사용

POST

새 리소스를 만들 때 사용

PUT

기존 리소스를 수정할 때 사용

PATCH

기존 리소스의 일부를 수정할 때 사용

DELETE

기존 리소스를 삭제할 떄 사용

+
+
+
+

HTTP 상태 코드

+
+
+

본 REST API에서 사용하는 HTTP 상태 코드는 가능한 표준 HTTP와 REST 규약을 따릅니다.

+
+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
상태 코드용례

200 OK

요청을 성공적으로 처리함

201 Created

새 리소스를 성공적으로 생성함. 응답의 Location 헤더에 해당 리소스의 URI가 담겨있다.

204 No Content

기존 리소스를 성공적으로 수정함.

400 Bad Request

잘못된 요청을 보낸 경우. 응답 본문에 더 오류에 대한 정보가 담겨있다.

404 Not Found

요청한 리소스가 없음.

409 Conflict

클라이언트의 요청이 서버의 상태와 충돌이 발생한 경우.

+
+
+
+

snippets 작성 컨벤션

+
+
+

domain-httpRequestCode-etc

+
+
+
+
+

주문

+
+
+

점주 서비스 - 주문 페이지

+
+
    +
  • +

    페이지 offset : 6

    +
  • +
+
+
+

Curl request

+
+
+
$ curl 'http://127.0.0.1:8001/orderMain?orderDate=2022-02-03&lastOrderId=7' -i -X GET
+
+
+
+
+

HTTP request

+
+
+
GET /orderMain?orderDate=2022-02-03&lastOrderId=7 HTTP/1.1
+Host: 127.0.0.1:8001
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 769
+
+{
+  "code" : "SUCCESS",
+  "message" : "",
+  "data" : [ {
+    "orderId" : 1,
+    "userId" : 1,
+    "userName" : "닉네임",
+    "orderItemResponses" : [ {
+      "orderItemId" : 100,
+      "itemId" : 100,
+      "itemName" : "아이템1"
+    }, {
+      "orderItemId" : 101,
+      "itemId" : 101,
+      "itemName" : "아이템2"
+    } ],
+    "orderStatus" : "PLACED",
+    "orderTime" : "2022-02-03 14:00:00"
+  }, {
+    "orderId" : 2,
+    "userId" : 1,
+    "userName" : "닉네임",
+    "orderItemResponses" : [ {
+      "orderItemId" : 102,
+      "itemId" : 102,
+      "itemName" : "아이템3"
+    }, {
+      "orderItemId" : 103,
+      "itemId" : 103,
+      "itemName" : "아이템2"
+    } ],
+    "orderStatus" : "CANCELED",
+    "orderTime" : "2022-02-03 15:00:00"
+  } ]
+}
+
+
+
+
+

Request parameters

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

orderDate

주문 날짜 YYYY-MM-DD

lastOrderId

페이지의 마지막 주문 고유 번호

+
+
+

Response fields

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

code

String

결과 코드 SUCCESS/ERROR

message

String

메시지

data[*].orderId

Number

주문 고유 번호

data[*].userId

Number

고객 고유 번호

data[*].userName

String

고객 이름

data[*].orderItemResponses[*].orderItemId

Number

장바구니 고유번호

data[*].orderItemResponses[*].itemId

Number

상품 고유번호

data[*].orderItemResponses[*].itemName

String

상품 이름

data[*].orderStatus

String

주문 상태

data[*].orderTime

String

주문 시간

+
+
+
+

점주 서비스 - 주문 페이지 (잘못된 파라미터 형식)

+
+

Curl request

+
+
+
$ curl 'http://127.0.0.1:8001/orderMain?orderDate=20220203&lastOrderId=7' -i -X GET
+
+
+
+
+

HTTP request

+
+
+
GET /orderMain?orderDate=20220203&lastOrderId=7 HTTP/1.1
+Host: 127.0.0.1:8001
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 400 Bad Request
+Content-Type: application/json
+Content-Length: 160
+
+{
+  "code" : "ERROR",
+  "message" : "[orderDate](은)는 YYYY-MM-DD 형식에 맞게 작성되지 않았습니다. 입력된 값: [20220203]",
+  "data" : null
+}
+
+
+
+
+

Request parameters

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

orderDate

주문 날짜 YYYY-MM-DD

lastOrderId

페이지의 마지막 주문 고유 번호

+
+
+

Response fields

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

code

String

결과 코드 SUCCESS/ERROR

message

String

메시지

data

Null

데이터

+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/order-service/src/test/java/com/justpickup/orderservice/config/TestConfig.java b/order-service/src/test/java/com/justpickup/orderservice/config/TestConfig.java new file mode 100644 index 0000000..9b17478 --- /dev/null +++ b/order-service/src/test/java/com/justpickup/orderservice/config/TestConfig.java @@ -0,0 +1,18 @@ +package com.justpickup.orderservice.config; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +public class TestConfig { + + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint()); + } +} diff --git a/order-service/src/test/java/com/justpickup/orderservice/domain/order/web/OrderControllerTest.java b/order-service/src/test/java/com/justpickup/orderservice/domain/order/web/OrderControllerTest.java new file mode 100644 index 0000000..345817f --- /dev/null +++ b/order-service/src/test/java/com/justpickup/orderservice/domain/order/web/OrderControllerTest.java @@ -0,0 +1,172 @@ +package com.justpickup.orderservice.domain.order.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.justpickup.orderservice.config.TestConfig; +import com.justpickup.orderservice.domain.order.dto.OrderDto; +import com.justpickup.orderservice.domain.order.dto.OrderSearchCondition; +import com.justpickup.orderservice.domain.order.entity.OrderStatus; +import com.justpickup.orderservice.domain.order.service.OrderService; +import com.justpickup.orderservice.domain.orderItem.dto.OrderItemDto; +import com.justpickup.orderservice.global.dto.Code; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.restdocs.request.RequestDocumentation.*; + +@WebMvcTest(OrderController.class) +@Import(TestConfig.class) +@AutoConfigureRestDocs(uriHost = "127.0.0.1", uriPort = 8001) +class OrderControllerTest { + + @Autowired + ObjectMapper objectMapper; + + @Autowired + MockMvc mockMvc; + + @MockBean + OrderService orderService; + + @Test + @DisplayName("점주 서비스 - 주문 페이지") + void getOrderMain() throws Exception { + // GIVEN + String orderDate = "2022-02-03"; + Long lastOrderId = 7L; + OrderSearchCondition condition = new OrderSearchCondition(orderDate, lastOrderId); + // TODO: 2022/02/07 jwt 구현 시 변경 요망 + Long storeId = 1L; + + given(orderService.findOrderMain(condition, storeId)) + .willReturn(getOrderMainDtoList()); + + // WHEN + ResultActions actions = mockMvc.perform(get("/orderMain") + .param("orderDate", orderDate) + .param("lastOrderId", String.valueOf(lastOrderId)) + ); + + // THEN + actions.andExpect(status().isOk()) + .andExpect(jsonPath("code").value(Code.SUCCESS.name())) + .andExpect(jsonPath("message").isEmpty()) + .andExpect(jsonPath("data").exists()) + .andExpect(jsonPath("data[*].orderItemResponses").exists()) + .andExpect(jsonPath("data[*].orderStatus").exists()) + .andExpect(jsonPath("data[*].orderTime").exists()) + .andDo(print()) + .andDo(document("orderMain-get", + requestParameters( + parameterWithName("orderDate").description("주문 날짜 YYYY-MM-DD"), + parameterWithName("lastOrderId").optional().description("페이지의 마지막 주문 고유 번호") + ), + responseFields( + fieldWithPath("code").description("결과 코드 SUCCESS/ERROR"), + fieldWithPath("message").description("메시지"), + fieldWithPath("data[*].orderId").description("주문 고유 번호"), + fieldWithPath("data[*].userId").description("고객 고유 번호"), + fieldWithPath("data[*].userName").description("고객 이름"), + fieldWithPath("data[*].orderItemResponses[*].orderItemId").description("장바구니 고유번호"), + fieldWithPath("data[*].orderItemResponses[*].itemId").description("상품 고유번호"), + fieldWithPath("data[*].orderItemResponses[*].itemName").description("상품 이름"), + fieldWithPath("data[*].orderStatus").description("주문 상태"), + fieldWithPath("data[*].orderTime").description("주문 시간") + ) + )) + ; + } + + @Test + @DisplayName("점주 서비스 - 주문 페이지 (잘못된 파라미터 형식)") + void getOrderMainBadRequestException() throws Exception { + // GIVEN + String orderDate = "20220203"; + Long lastOrderId = 7L; + + // WHEN + ResultActions actions = mockMvc.perform(get("/orderMain") + .param("orderDate", orderDate) + .param("lastOrderId", String.valueOf(lastOrderId)) + ); + + // THEN + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("code").value(Code.ERROR.name())) + .andExpect(jsonPath("message").isNotEmpty()) + .andExpect(jsonPath("data").isEmpty()) + .andDo(print()) + .andDo(document("orderMain-get-badParameterException", + requestParameters( + parameterWithName("orderDate").description("주문 날짜 YYYY-MM-DD"), + parameterWithName("lastOrderId").optional().description("페이지의 마지막 주문 고유 번호") + ), + responseFields( + fieldWithPath("code").description("결과 코드 SUCCESS/ERROR"), + fieldWithPath("message").description("메시지"), + fieldWithPath("data").description("데이터") + ) + ) + ) + ; + } + + private List getOrderMainDtoList() { + OrderItemDto orderItemDto_100 = OrderItemDto.builder() + .id(100L) + .itemId(100L) + .build(); + orderItemDto_100.setItemName("아이템1"); + OrderItemDto orderItemDto_101 = OrderItemDto.builder() + .id(101L) + .itemId(101L) + .build(); + orderItemDto_101.setItemName("아이템2"); + OrderItemDto orderItemDto_102 = OrderItemDto.builder() + .id(102L) + .itemId(102L) + .build(); + orderItemDto_102.setItemName("아이템3"); + OrderItemDto orderItemDto_103 = OrderItemDto.builder() + .id(103L) + .itemId(103L) + .build(); + orderItemDto_103.setItemName("아이템2"); + + OrderDto orderDto_1 = OrderDto.builder() + .id(1L) + .userId(1L) + .orderItemDtoList(List.of(orderItemDto_100, orderItemDto_101)) + .orderStatus(OrderStatus.PLACED) + .orderTime(LocalDateTime.of(2022, 2, 3, 14, 0, 0)) + .build(); + orderDto_1.setUserName("닉네임"); + OrderDto orderDto_2 = OrderDto.builder() + .id(2L) + .userId(1L) + .orderItemDtoList(List.of(orderItemDto_102, orderItemDto_103)) + .orderStatus(OrderStatus.CANCELED) + .orderTime(LocalDateTime.of(2022, 2, 3, 15, 0, 0)) + .build(); + orderDto_2.setUserName("닉네임"); + + return List.of(orderDto_1, orderDto_2); + } +} \ No newline at end of file diff --git a/owner-frontend-service/src/main/resources/static/common.js b/owner-frontend-service/src/main/resources/static/common.js index 9134f34..42388d1 100644 --- a/owner-frontend-service/src/main/resources/static/common.js +++ b/owner-frontend-service/src/main/resources/static/common.js @@ -1,4 +1,4 @@ -const ownerApiGatewayIp = "http://127.0.0.1:8001/"; +const ownerApiGatewayIp = "http://127.0.0.1:8001"; const url = { - orderService : ownerApiGatewayIp + "order-service/" + orderService : ownerApiGatewayIp + "/order-service" } \ No newline at end of file diff --git a/owner-frontend-service/src/main/resources/templates/domain/order/order.html b/owner-frontend-service/src/main/resources/templates/domain/order/order.html index 7255fa2..23402b8 100644 --- a/owner-frontend-service/src/main/resources/templates/domain/order/order.html +++ b/owner-frontend-service/src/main/resources/templates/domain/order/order.html @@ -12,93 +12,159 @@

주문

- -
2022년 01년 14일
+
+
+
+ +
+
+ +
+ +
+
-
- -
-
-
- 닉네임 -
-
-
- - 메뉴1, 메뉴2, 메뉴3 - -
-

- 오전 11:03 -

- 상세보기 -
- -
-
- +
- -
-
-
- 닉네임 -
-
-
- - 메뉴1, 메뉴2, 메뉴3, 메뉴4, 메뉴5, 메뉴6 - -
-

- 오후 04:03 -

- 상세보기 -
- -
+
+ - - - -
-
-
- 닉네임 -
-
-
- - 메뉴1, 메뉴2 - -
-

- 오후 10:45 -

- 상세보기 -
- -
-
- -
-
diff --git a/owner-frontend-service/src/main/resources/templates/fragments/left.html b/owner-frontend-service/src/main/resources/templates/fragments/left.html index b49bc61..f788360 100644 --- a/owner-frontend-service/src/main/resources/templates/fragments/left.html +++ b/owner-frontend-service/src/main/resources/templates/fragments/left.html @@ -31,25 +31,25 @@
diff --git a/store-service/src/docs/asciidoc/api-docs.adoc b/store-service/src/docs/asciidoc/api-docs.adoc new file mode 100644 index 0000000..15ce2f7 --- /dev/null +++ b/store-service/src/docs/asciidoc/api-docs.adoc @@ -0,0 +1,71 @@ +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 2 +:sectlinks: + + +[[overview]] += 개요 + +[[overview-http-verbs]] +== HTTP 동사 + +본 REST API에서 사용하는 HTTP 동사(verbs)는 가능한한 표준 HTTP와 REST 규약을 따릅니다. + +|=== +| 동사 | 용례 + +| `GET` +| 리소스를 가져올 때 사용 + +| `POST` +| 새 리소스를 만들 때 사용 + +| `PUT` +| 기존 리소스를 수정할 때 사용 + +| `PATCH` +| 기존 리소스의 일부를 수정할 때 사용 + +| `DELETE` +| 기존 리소스를 삭제할 떄 사용 +|=== + +[[overview-http-status-codes]] +== HTTP 상태 코드 + +본 REST API에서 사용하는 HTTP 상태 코드는 가능한 표준 HTTP와 REST 규약을 따릅니다. + +|=== +| 상태 코드 | 용례 + +| `200 OK` +| 요청을 성공적으로 처리함 + +| `201 Created` +| 새 리소스를 성공적으로 생성함. 응답의 `Location` 헤더에 해당 리소스의 URI가 담겨있다. + +| `204 No Content` +| 기존 리소스를 성공적으로 수정함. + +| `400 Bad Request` +| 잘못된 요청을 보낸 경우. 응답 본문에 더 오류에 대한 정보가 담겨있다. + +| `404 Not Found` +| 요청한 리소스가 없음. + +| `409 Conflict` +| 클라이언트의 요청이 서버의 상태와 충돌이 발생한 경우. +|=== + +[[snippets-write-convention]] +== snippets 작성 컨벤션 +domain-httpRequestCode-etc + +== 상품 +=== 상품 조회 +operation::item-get[snippets='curl-request,http-request,http-response,path-parameters,response-fields'] +=== 상품 조회 (존재하지 않는 상품) +operation::item-get-notExistItemException[snippets='curl-request,http-request,http-response,path-parameters,response-fields'] \ No newline at end of file diff --git a/store-service/src/main/java/com/justpickup/storeservice/domain/item/exception/NotExistItemException.java b/store-service/src/main/java/com/justpickup/storeservice/domain/item/exception/NotExistItemException.java new file mode 100644 index 0000000..22450dd --- /dev/null +++ b/store-service/src/main/java/com/justpickup/storeservice/domain/item/exception/NotExistItemException.java @@ -0,0 +1,11 @@ +package com.justpickup.storeservice.domain.item.exception; + +import com.justpickup.storeservice.global.exception.CustomException; +import org.springframework.http.HttpStatus; + +public class NotExistItemException extends CustomException { + + public NotExistItemException(String message) { + super(HttpStatus.CONFLICT, message); + } +} diff --git a/store-service/src/main/java/com/justpickup/storeservice/domain/item/service/ItemServiceImpl.java b/store-service/src/main/java/com/justpickup/storeservice/domain/item/service/ItemServiceImpl.java index 1500450..9722720 100644 --- a/store-service/src/main/java/com/justpickup/storeservice/domain/item/service/ItemServiceImpl.java +++ b/store-service/src/main/java/com/justpickup/storeservice/domain/item/service/ItemServiceImpl.java @@ -2,14 +2,13 @@ package com.justpickup.storeservice.domain.item.service; import com.justpickup.storeservice.domain.item.dto.ItemDto; import com.justpickup.storeservice.domain.item.entity.Item; +import com.justpickup.storeservice.domain.item.exception.NotExistItemException; import com.justpickup.storeservice.domain.item.repository.ItemRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.NoSuchElementException; - @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -22,7 +21,7 @@ public class ItemServiceImpl implements ItemService { @Override public ItemDto findItemByItemId(Long itemId) { Item findItem = itemRepository.findById(itemId) - .orElseThrow(NoSuchElementException::new); + .orElseThrow(() -> new NotExistItemException("존재하지 않는 아이템 입니다.")); return ItemDto.createItemDto(findItem); } diff --git a/store-service/src/main/resources/static/docs/api-docs.html b/store-service/src/main/resources/static/docs/api-docs.html new file mode 100644 index 0000000..590a541 --- /dev/null +++ b/store-service/src/main/resources/static/docs/api-docs.html @@ -0,0 +1,778 @@ + + + + + + + +개요 + + + + + + +
+
+

HTTP 동사

+
+
+

본 REST API에서 사용하는 HTTP 동사(verbs)는 가능한한 표준 HTTP와 REST 규약을 따릅니다.

+
+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
동사용례

GET

리소스를 가져올 때 사용

POST

새 리소스를 만들 때 사용

PUT

기존 리소스를 수정할 때 사용

PATCH

기존 리소스의 일부를 수정할 때 사용

DELETE

기존 리소스를 삭제할 떄 사용

+
+
+
+

HTTP 상태 코드

+
+
+

본 REST API에서 사용하는 HTTP 상태 코드는 가능한 표준 HTTP와 REST 규약을 따릅니다.

+
+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
상태 코드용례

200 OK

요청을 성공적으로 처리함

201 Created

새 리소스를 성공적으로 생성함. 응답의 Location 헤더에 해당 리소스의 URI가 담겨있다.

204 No Content

기존 리소스를 성공적으로 수정함.

400 Bad Request

잘못된 요청을 보낸 경우. 응답 본문에 더 오류에 대한 정보가 담겨있다.

404 Not Found

요청한 리소스가 없음.

409 Conflict

클라이언트의 요청이 서버의 상태와 충돌이 발생한 경우.

+
+
+
+

snippets 작성 컨벤션

+
+
+

domain-httpRequestCode-etc

+
+
+
+
+

상품

+
+
+

상품 조회

+
+

Curl request

+
+
+
$ curl 'http://127.0.0.1:8001/item/1' -i -X GET
+
+
+
+
+

HTTP request

+
+
+
GET /item/1 HTTP/1.1
+Host: 127.0.0.1:8001
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 146
+
+{
+  "code" : "SUCCESS",
+  "message" : "",
+  "data" : {
+    "id" : 1,
+    "name" : "아메리카노",
+    "salesYn" : "Y",
+    "price" : 1500
+  }
+}
+
+
+
+
+

Path parameters

+ + ++++ + + + + + + + + + + + + +
Table 1. /item/{itemId}
ParameterDescription

itemId

상품 고유 번호

+
+
+

Response fields

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

code

String

결과 코드 SUCCESS/ERROR

message

String

메시지

data.id

Number

상품 고유 번호

data.name

String

상품 이름

data.salesYn

String

화면 표시 여부 Y/N

data.price

Number

상품 가격

+
+
+
+

상품 조회 (존재하지 않는 상품)

+
+

Curl request

+
+
+
$ curl 'http://127.0.0.1:8001/item/9999' -i -X GET
+
+
+
+
+

HTTP request

+
+
+
GET /item/9999 HTTP/1.1
+Host: 127.0.0.1:8001
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 409 Conflict
+Content-Type: application/json
+Content-Length: 93
+
+{
+  "code" : "ERROR",
+  "message" : "존재하지 않는 상품입니다.",
+  "data" : null
+}
+
+
+
+
+

Path parameters

+ + ++++ + + + + + + + + + + + + +
Table 1. /item/{itemId}
ParameterDescription

itemId

상품 고유 번호

+
+
+

Response fields

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

code

String

결과 코드 SUCCESS/ERROR

message

String

메시지

data

Null

데이터

+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/store-service/src/test/java/com/justpickup/storeservice/config/TestConfig.java b/store-service/src/test/java/com/justpickup/storeservice/config/TestConfig.java new file mode 100644 index 0000000..4440687 --- /dev/null +++ b/store-service/src/test/java/com/justpickup/storeservice/config/TestConfig.java @@ -0,0 +1,18 @@ +package com.justpickup.storeservice.config; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +public class TestConfig { + + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint()); + } +} diff --git a/store-service/src/test/java/com/justpickup/storeservice/domain/item/web/ItemControllerTest.java b/store-service/src/test/java/com/justpickup/storeservice/domain/item/web/ItemControllerTest.java new file mode 100644 index 0000000..32baaf1 --- /dev/null +++ b/store-service/src/test/java/com/justpickup/storeservice/domain/item/web/ItemControllerTest.java @@ -0,0 +1,118 @@ +package com.justpickup.storeservice.domain.item.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.justpickup.storeservice.config.TestConfig; +import com.justpickup.storeservice.domain.item.dto.ItemDto; +import com.justpickup.storeservice.domain.item.exception.NotExistItemException; +import com.justpickup.storeservice.domain.item.service.ItemService; +import com.justpickup.storeservice.global.dto.Code; +import com.justpickup.storeservice.global.entity.Yn; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +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.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(ItemController.class) +@Import(TestConfig.class) +@AutoConfigureRestDocs(uriHost = "127.0.0.1", uriPort = 8001) +class ItemControllerTest { + + @Autowired + ObjectMapper objectMapper; + + @Autowired + MockMvc mockMvc; + + @MockBean + ItemService itemService; + + @Test + @DisplayName("상품 조회") + void getItem() throws Exception { + // GIVEN + long itemId = 1L; + ItemDto willReturnDto = ItemDto.builder() + .id(1L) + .salesYn(Yn.Y) + .price(1500L) + .name("아메리카노") + .build(); + given(itemService.findItemByItemId(itemId)) + .willReturn(willReturnDto); + + // WHEN + ResultActions actions = mockMvc.perform(get("/item/{itemId}", itemId)); + + // THEN + actions.andExpect(status().isOk()) + .andExpect(jsonPath("code").value(Code.SUCCESS.name())) + .andExpect(jsonPath("message").value("")) + .andExpect(jsonPath("data.id").value(itemId)) + .andExpect(jsonPath("data.name").value("아메리카노")) + .andExpect(jsonPath("data.salesYn").value(Yn.Y.name())) + .andExpect(jsonPath("data.price").value(1500)) + .andDo(print()) + .andDo(document("item-get", + pathParameters( + parameterWithName("itemId").description("상품 고유 번호") + ), + responseFields( + fieldWithPath("code").description("결과 코드 SUCCESS/ERROR"), + fieldWithPath("message").description("메시지"), + fieldWithPath("data.id").description("상품 고유 번호"), + fieldWithPath("data.name").description("상품 이름"), + fieldWithPath("data.salesYn").description("화면 표시 여부 Y/N"), + fieldWithPath("data.price").description("상품 가격") + ) + )) + ; + } + + @Test + @DisplayName("상품 조회 - 존재하지 않는 상품") + void getItemNotExistItemException() throws Exception { + // GIVEN + long notExistItemId = 9999L; + String message = "존재하지 않는 상품입니다."; + given(itemService.findItemByItemId(notExistItemId)) + .willThrow(new NotExistItemException(message)); + + // THEN + ResultActions actions = mockMvc.perform(get("/item/{itemId}", notExistItemId)); + + // WHEN + actions.andExpect(status().isConflict()) + .andExpect(jsonPath("code").value(Code.ERROR.name())) + .andExpect(jsonPath("message").value(message)) + .andExpect(jsonPath("data").isEmpty()) + .andDo(print()) + .andDo(document("item-get-notExistItemException", + pathParameters( + parameterWithName("itemId").description("상품 고유 번호") + ), + responseFields( + fieldWithPath("code").description("결과 코드 SUCCESS/ERROR"), + fieldWithPath("message").description("메시지"), + fieldWithPath("data").description("데이터") + ) + )) + ; + } +} \ No newline at end of file diff --git a/user-service/src/docs/asciidoc/api-docs.adoc b/user-service/src/docs/asciidoc/api-docs.adoc new file mode 100644 index 0000000..dfde993 --- /dev/null +++ b/user-service/src/docs/asciidoc/api-docs.adoc @@ -0,0 +1,71 @@ +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 2 +:sectlinks: + + +[[overview]] += 개요 + +[[overview-http-verbs]] +== HTTP 동사 + +본 REST API에서 사용하는 HTTP 동사(verbs)는 가능한한 표준 HTTP와 REST 규약을 따릅니다. + +|=== +| 동사 | 용례 + +| `GET` +| 리소스를 가져올 때 사용 + +| `POST` +| 새 리소스를 만들 때 사용 + +| `PUT` +| 기존 리소스를 수정할 때 사용 + +| `PATCH` +| 기존 리소스의 일부를 수정할 때 사용 + +| `DELETE` +| 기존 리소스를 삭제할 떄 사용 +|=== + +[[overview-http-status-codes]] +== HTTP 상태 코드 + +본 REST API에서 사용하는 HTTP 상태 코드는 가능한 표준 HTTP와 REST 규약을 따릅니다. + +|=== +| 상태 코드 | 용례 + +| `200 OK` +| 요청을 성공적으로 처리함 + +| `201 Created` +| 새 리소스를 성공적으로 생성함. 응답의 `Location` 헤더에 해당 리소스의 URI가 담겨있다. + +| `204 No Content` +| 기존 리소스를 성공적으로 수정함. + +| `400 Bad Request` +| 잘못된 요청을 보낸 경우. 응답 본문에 더 오류에 대한 정보가 담겨있다. + +| `404 Not Found` +| 요청한 리소스가 없음. + +| `409 Conflict` +| 클라이언트의 요청이 서버의 상태와 충돌이 발생한 경우. +|=== + +[[snippets-write-convention]] +== snippets 작성 컨벤션 +domain-httpRequestCode-etc + +== 유저 +=== 회원 조회 +operation::customer-get[snippets='curl-request,http-request,http-response,path-parameters,response-fields'] +=== 회원 조회 (존재하지 않는 회원) +operation::customer-get-notExistUserException[snippets='curl-request,http-request,http-response,path-parameters,response-fields'] \ No newline at end of file diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/CustomerDto.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/CustomerDto.java index dabe187..ac60aa5 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/CustomerDto.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/CustomerDto.java @@ -1,6 +1,7 @@ package com.justpickup.userservice.domain.user.dto; import com.justpickup.userservice.domain.user.entity.Customer; +import lombok.Builder; import lombok.Getter; @Getter @@ -9,4 +10,9 @@ public class CustomerDto extends UserDto { public CustomerDto(Customer customer) { super(customer); } + + @Builder + public CustomerDto(Long id, String password, String name, String phoneNumber) { + super(id, password, name, phoneNumber); + } } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/UserDto.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/UserDto.java index 60596ee..3fad831 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/UserDto.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/UserDto.java @@ -1,6 +1,7 @@ package com.justpickup.userservice.domain.user.dto; import com.justpickup.userservice.domain.user.entity.Customer; +import lombok.Builder; import lombok.Getter; @Getter @@ -17,4 +18,11 @@ public class UserDto { this.name = customer.getName(); this.phoneNumber = customer.getPhoneNumber(); } + + public UserDto(Long id, String password, String name, String phoneNumber) { + this.id = id; + this.password = password; + this.name = name; + this.phoneNumber = phoneNumber; + } } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/web/UserController.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/web/UserController.java index 53c6431..ba35d4b 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/web/UserController.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/web/UserController.java @@ -14,6 +14,8 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; +import javax.validation.Valid; + @RestController @RequiredArgsConstructor @Slf4j @@ -22,7 +24,7 @@ public class UserController { private final UserService userService; @GetMapping("/customer/{userId}") - public ResponseEntity getCustomer(@PathVariable("userId") Long userId) { + public ResponseEntity getCustomer(@Valid @PathVariable("userId") Long userId) { CustomerDto customerDto = userService.findCustomerByUserId(userId); diff --git a/user-service/src/main/resources/static/docs/api-docs.html b/user-service/src/main/resources/static/docs/api-docs.html new file mode 100644 index 0000000..c8eb9e2 --- /dev/null +++ b/user-service/src/main/resources/static/docs/api-docs.html @@ -0,0 +1,772 @@ + + + + + + + +개요 + + + + + + +
+
+

HTTP 동사

+
+
+

본 REST API에서 사용하는 HTTP 동사(verbs)는 가능한한 표준 HTTP와 REST 규약을 따릅니다.

+
+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
동사용례

GET

리소스를 가져올 때 사용

POST

새 리소스를 만들 때 사용

PUT

기존 리소스를 수정할 때 사용

PATCH

기존 리소스의 일부를 수정할 때 사용

DELETE

기존 리소스를 삭제할 떄 사용

+
+
+
+

HTTP 상태 코드

+
+
+

본 REST API에서 사용하는 HTTP 상태 코드는 가능한 표준 HTTP와 REST 규약을 따릅니다.

+
+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
상태 코드용례

200 OK

요청을 성공적으로 처리함

201 Created

새 리소스를 성공적으로 생성함. 응답의 Location 헤더에 해당 리소스의 URI가 담겨있다.

204 No Content

기존 리소스를 성공적으로 수정함.

400 Bad Request

잘못된 요청을 보낸 경우. 응답 본문에 더 오류에 대한 정보가 담겨있다.

404 Not Found

요청한 리소스가 없음.

409 Conflict

클라이언트의 요청이 서버의 상태와 충돌이 발생한 경우.

+
+
+
+

snippets 작성 컨벤션

+
+
+

domain-httpRequestCode-etc

+
+
+
+
+

유저

+
+
+

회원 조회

+
+

Curl request

+
+
+
$ curl 'http://127.0.0.1:8001/customer/1' -i -X GET
+
+
+
+
+

HTTP request

+
+
+
GET /customer/1 HTTP/1.1
+Host: 127.0.0.1:8001
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 141
+
+{
+  "code" : "SUCCESS",
+  "message" : "",
+  "data" : {
+    "userId" : 1,
+    "userName" : "이름",
+    "phoneNumber" : "010-1234-5678"
+  }
+}
+
+
+
+
+

Path parameters

+ + ++++ + + + + + + + + + + + + +
Table 1. /customer/{userId}
ParameterDescription

userId

회원 고유번호

+
+
+

Response fields

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

code

String

결과코드 SUCCESS/ERROR

message

String

메시지

data.userId

Number

회원 고유번호

data.userName

String

회원 이름

data.phoneNumber

String

회원 휴대폰 번호

+
+
+
+

회원 조회 (존재하지 않는 회원)

+
+

Curl request

+
+
+
$ curl 'http://127.0.0.1:8001/customer/9999' -i -X GET
+
+
+
+
+

HTTP request

+
+
+
GET /customer/9999 HTTP/1.1
+Host: 127.0.0.1:8001
+
+
+
+
+

HTTP response

+
+
+
HTTP/1.1 409 Conflict
+Content-Type: application/json
+Content-Length: 94
+
+{
+  "code" : "ERROR",
+  "message" : "존재하지 않는 회원 입니다.",
+  "data" : null
+}
+
+
+
+
+

Path parameters

+ + ++++ + + + + + + + + + + + + +
Table 1. /customer/{userId}
ParameterDescription

userId

회원 고유번호

+
+
+

Response fields

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

code

String

결과코드 SUCCESS/ERROR

message

String

메시지

data

Null

데이터

+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/user-service/src/test/java/com/justpickup/userservice/config/TestConfig.java b/user-service/src/test/java/com/justpickup/userservice/config/TestConfig.java new file mode 100644 index 0000000..4a67747 --- /dev/null +++ b/user-service/src/test/java/com/justpickup/userservice/config/TestConfig.java @@ -0,0 +1,18 @@ +package com.justpickup.userservice.config; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +public class TestConfig { + + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint()); + } +} diff --git a/user-service/src/test/java/com/justpickup/userservice/domain/user/web/UserControllerTest.java b/user-service/src/test/java/com/justpickup/userservice/domain/user/web/UserControllerTest.java new file mode 100644 index 0000000..d27be15 --- /dev/null +++ b/user-service/src/test/java/com/justpickup/userservice/domain/user/web/UserControllerTest.java @@ -0,0 +1,118 @@ +package com.justpickup.userservice.domain.user.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.justpickup.userservice.config.TestConfig; +import com.justpickup.userservice.domain.user.dto.CustomerDto; +import com.justpickup.userservice.domain.user.exception.NotExistUserException; +import com.justpickup.userservice.domain.user.service.UserService; +import com.justpickup.userservice.global.dto.Code; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +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.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@WebMvcTest(UserController.class) +@Import(TestConfig.class) +@AutoConfigureRestDocs(uriHost = "127.0.0.1", uriPort = 8001) +class UserControllerTest { + + @Autowired + ObjectMapper objectMapper; + + @Autowired + MockMvc mockMvc; + + @MockBean + UserService userService; + + @Test + @DisplayName("회원 조회") + void getCustomer() throws Exception { + // GIVEN + long userId = 1L; + + CustomerDto willReturnDto = CustomerDto.builder() + .id(1L) + .name("이름") + .password("password!@#") + .phoneNumber("010-1234-5678") + .build(); + + given(userService.findCustomerByUserId(userId)) + .willReturn(willReturnDto); + + // WHEN + ResultActions actions = mockMvc.perform(get("/customer/{userId}", userId)); + + // THEN + actions.andExpect(status().isOk()) + .andExpect(jsonPath("code").value(Code.SUCCESS.name())) + .andExpect(jsonPath("message").value("")) + .andExpect(jsonPath("data.userId").value(1)) + .andExpect(jsonPath("data.userName").value("이름")) + .andExpect(jsonPath("data.phoneNumber").value("010-1234-5678")) + .andDo(print()) + .andDo(document("customer-get", + pathParameters( + parameterWithName("userId").description("회원 고유번호") + ), + responseFields( + fieldWithPath("code").description("결과코드 SUCCESS/ERROR"), + fieldWithPath("message").description("메시지"), + fieldWithPath("data.userId").description("회원 고유번호"), + fieldWithPath("data.userName").description("회원 이름"), + fieldWithPath("data.phoneNumber").description("회원 휴대폰 번호") + )) + ) + ; + } + + @Test + @DisplayName("회원 조회 - 존재하지 않는 회원") + void getCustomerNotExistUserException() throws Exception { + // GIVEN + long notExistUserId = 9999L; + String message = "존재하지 않는 회원 입니다."; + given(userService.findCustomerByUserId(notExistUserId)) + .willThrow(new NotExistUserException(message)); + + // WHEN + ResultActions actions = mockMvc.perform(get("/customer/{userId}", notExistUserId)); + + // THEN + actions.andExpect(status().isConflict()) + .andExpect(jsonPath("code").value(Code.ERROR.name())) + .andExpect(jsonPath("message").value(message)) + .andExpect(jsonPath("data").isEmpty()) + .andDo(print()) + .andDo(document("customer-get-notExistUserException", + pathParameters( + parameterWithName("userId").description("회원 고유번호") + ), + responseFields( + fieldWithPath("code").description("결과코드 SUCCESS/ERROR"), + fieldWithPath("message").description("메시지"), + fieldWithPath("data").description("데이터") + ) + )) + ; + } +} \ No newline at end of file