Merge pull request #58 from Development-team-1/owner_dashboard

Owner dashboard
This commit is contained in:
Sangbum Park
2022-03-23 16:00:58 +09:00
committed by GitHub
38 changed files with 802 additions and 92 deletions

View File

@@ -24,6 +24,9 @@ export default {
getOrder() {
return axios.get(process.env.VUE_APP_ORDER_API_URL + "/order/orders");
},
deleteOrderItem(orderItemId){
return axios.delete(process.env.VUE_APP_ORDER_API_URL + "/orderItem/"+orderItemId);
},
getOrderDetail(orderId) {
return axios.get(process.env.VUE_APP_CUSTOMER_SERVICE_BASEURL + "/order-service/api/order-detail/" + orderId);
}

View File

@@ -9,8 +9,9 @@
<v-app-bar-nav-icon @click="$router.back()">
<v-icon>mdi-arrow-left</v-icon>
</v-app-bar-nav-icon>
<v-spacer></v-spacer>
<v-toolbar-title>
<v-toolbar-title style="position: absolute; left: 50%; transform:translateX(-50%);">
<v-img :src="require('@/assets/just-logo.png')"></v-img>
</v-toolbar-title>
<v-spacer></v-spacer>

View File

@@ -11,7 +11,7 @@
<v-icon>mdi-arrow-left</v-icon>
</v-app-bar-nav-icon>
<v-spacer></v-spacer>
<v-toolbar-title>
<v-toolbar-title style="position: absolute; left: 50%; transform:translateX(-50%);">
<b>{{store.name}}</b>
</v-toolbar-title>
<v-spacer></v-spacer>
@@ -23,11 +23,19 @@
:color="favoriteStore.color"
>{{ favoriteStore.icon }}</v-icon>
</v-btn>
<v-btn
color="white"
elevation="0"
@click="logout"
>
<v-icon>mdi-logout</v-icon>
</v-btn>
</v-app-bar>
</template>
<script>
import storeApi from "@/api/store";
import authApi from "@/api/auth";
export default {
name: "StoreNavigation",
@@ -71,7 +79,14 @@ export default {
}
this.favoriteStore.status= !this.favoriteStore.status
},
logout: function () {
if(confirm("로그아웃하시겠습니까?")){
authApi.logout();
}
}
},
mounted() {
console.log(this.$route.params)

View File

@@ -64,12 +64,12 @@ const routes = [
name: 'notification',
component: () => import('../views/NotificationView')
},
{
path: "/item/:itemId",
beforeEnter: authCheck,
name: 'itemDetail',
component: () => import('../views/ItemDetail')
},
// {
// path: "/item/:itemId",
// beforeEnter: authCheck,
// name: 'itemDetail',
// component: () => import('../views/ItemDetail')
// },
{
path: "/order",
beforeEnter: authCheck,
@@ -110,6 +110,19 @@ const routes = [
},
]
},
{
path: '/item',
redirect: 'item',
component: StoreLayout,
children: [
{
path: "/item/:itemId",
beforeEnter: authCheck,
name: 'itemDetail',
component: () => import('../views/ItemDetail')
},
]
},
{
path: '/auth',
name: 'auth',

View File

@@ -116,7 +116,6 @@ export default {
storeId:-1,
itemId:-1,
price:0,
requireOption:{},
otherOptions:[],
},
}

View File

@@ -8,17 +8,6 @@
<router-view
v-on:getStoreId="renderNavigation">
</router-view>
<div align="right" >
<v-btn
color="primary"
dark
right
fab
@click="toOrder"
>
<v-icon>mdi-basket</v-icon>
</v-btn>
</div>
</v-container>
</v-main>
<bottom-navigation></bottom-navigation>
@@ -56,11 +45,6 @@ export default {
const response = await storeApi.requestStore(this.store.id);
this.store = response.data.data;
},
toOrder(){
if(confirm("주문화면으로 이동할까요?")){
this.$router.push("/order")
}
}
}
}

View File

@@ -43,7 +43,9 @@
/>
<v-btn
color="orange"
block>수정하기</v-btn>
block
@click="message('준비중입니다.')"
>수정하기</v-btn>
</v-form>
</v-card-text>
</v-card>
@@ -76,6 +78,9 @@ export default {
console.log(error.response)
})
},
message: function(message){
alert(message)
},
},
mounted() {
this.getUserData()

View File

@@ -8,7 +8,7 @@
<v-row>
<v-col
v-for=" orderItem in orderData.orderItemDtoList"
:key = "orderItem.itemId"
:key = "orderItem.id"
>
<v-card
class="mx-auto mb-5"
@@ -25,7 +25,7 @@
<div class="text-body-1 mb-5" style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">
{{ orderItem.orderItemOptionDtoList ?
orderItem.orderItemOptionDtoList.map(x=>x.name).join(', ')
: null}}
: null}} &nbsp;
</div>
<div class="text--primary">
합계 : <b> {{ orderItem.count * orderItem.price | currency}} </b>
@@ -40,7 +40,7 @@
</v-list-item>
<v-card-actions class="pb-2">
<v-btn block color="warning" @click="message('준비중입니다.')">삭제하기</v-btn>
<v-btn block color="warning" @click="deleteOrderItem(orderItem)">삭제하기</v-btn>
</v-card-actions>
</v-card>
@@ -98,7 +98,6 @@ export default {
getOrder: function(){
orderApi.getOrder()
.then(response=>{
console.log(response)
this.orderData=response.data.data
})
.catch(error=>{
@@ -107,9 +106,27 @@ export default {
console.log(error.response)
})
},
message: function(message){
alert(message)
}
deleteOrderItem: function(orderItem){
var vm = this
const deleteOrderItemId = orderItem.id
console.log(deleteOrderItemId)
orderApi.deleteOrderItem(deleteOrderItemId)
.then(response=>{
console.log(response)
alert(response.data.data+"삭제되었습니다.")
vm.orderData.orderPrice -=orderItem.price
vm.orderData.orderItemDtoList.splice(
vm.orderData.orderItemDtoList.indexOf(orderItem),1
)
if(vm.orderData.orderItemDtoList.length ==0)
this.$router.back()
})
.catch(error=>{
alert("문제가 발생하였습니다.")
console.log(error.response)
})
},
}
}
</script>

View File

@@ -54,6 +54,17 @@
<br>
</div>
</div>
<div align="right" >
<v-btn
color="primary"
dark
right
fab
@click="toOrder"
>
<v-icon>mdi-basket</v-icon>
</v-btn>
</div>
</div>
</template>
@@ -94,6 +105,11 @@ export default {
},
itemDetail: function (itemId) {
this.$router.push("/item/"+itemId)
},
toOrder(){
if(confirm("주문화면으로 이동할까요?")){
this.$router.push("/order")
}
}
}
}

View File

@@ -85,6 +85,9 @@ operation::prevOrder-get[snippets='curl-request,http-request,http-response,reque
=== 점주 서비스 - 지난 주문 페이지 (잘못된 파라미터 형식)
operation::prevOrder-get-BindException[snippets='curl-request,http-request,http-response,request-headers,request-parameters,response-fields']
=== 점주 서비스 - 대쉬보드
operation::owner-findDashboard[snippets='curl-request,http-request,http-response,request-headers,response-fields']
== Just Pick-up
=== 주문 내역 페이지
operation::api-customer-order-history[snippets='curl-request,http-request,http-response,request-headers,request-parameters,response-fields']
@@ -95,5 +98,9 @@ operation::add-item-to-basket[snippets='curl-request,http-request,http-response,
=== 장바구니 내역 가져오기
operation::fetch-order[snippets='curl-request,http-request,http-response,request-headers,response-fields']
=== 장바구니 상품 삭제
operation::delete-orderItem[snippets='curl-request,http-request,http-response,request-headers,path-parameters,response-fields']
=== 주문하기
operation::save-order[snippets='curl-request,http-request,http-response,request-headers']

View File

@@ -0,0 +1,58 @@
package com.justpickup.orderservice.domain.order.dto;
import lombok.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DashBoardDto {
//일일 판매금액
private Long salesAmount=0L;
private BestSellItem bestSellItem;
List<SellAmountAWeek> sellAmountAWeeks;
public static DashBoardDto of(List<OrderPrice> orderPrices , DashBoardDto.BestSellItem bestSellItem , List<SellAmountAWeek> sellAmountAWeeks){
DashBoardDto dashBoardDto = new DashBoardDto();
orderPrices.forEach(orderPrice -> dashBoardDto.salesAmount+=orderPrice.getOrderPrice());
dashBoardDto.bestSellItem = bestSellItem;
dashBoardDto.sellAmountAWeeks = sellAmountAWeeks;
return dashBoardDto;
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class OrderPrice{
Long orderPrice;
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class BestSellItem{
Long itemId;
String itemName;
Long sumCounts;
public void setItemName(String itemName) {
this.itemName = itemName;
}
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class SellAmountAWeek{
Object sellDate;
Long sellAmount;
}
}

View File

@@ -1,10 +1,14 @@
package com.justpickup.orderservice.domain.order.dto;
import com.justpickup.orderservice.domain.orderItem.entity.OrderItem;
import com.justpickup.orderservice.domain.orderItemOption.dto.OrderItemOptionDto;
import com.justpickup.orderservice.domain.orderItemOption.entity.OrderItemOption;
import com.justpickup.orderservice.global.client.store.GetItemResponse;
import com.justpickup.orderservice.global.client.store.OptionType;
import lombok.*;
import java.util.List;
import java.util.stream.Collectors;
@Getter
@NoArgsConstructor
@@ -33,7 +37,9 @@ public class FetchOrderDto {
private String itemName;
private List<GetItemResponse.ItemOptionDto> orderItemOptionDtoList;
private List<OrderItemOptionDto> orderItemOptionDtoList;
// private List<GetItemResponse.ItemOptionDto> orderItemOptionDtoList;
private Long price;
@@ -43,10 +49,35 @@ public class FetchOrderDto {
this.id = orderItem.getId();
this.itemId = getItemResponse.getId();
this.itemName = getItemResponse.getName();
this.orderItemOptionDtoList = getItemResponse.getItemOptions();
//getItemResponse에는 해당 item에 존재하는 itemOption들이 전부 들어있으므로, orderItem에서 orderItemOption에 있는값들을 가져와서 매칭해줌
this.orderItemOptionDtoList = orderItem.getOrderItemOptions().stream().map(orderItemOption -> {
OrderItemOptionDto orderItemOptionDto = new OrderItemOptionDto(orderItemOption.getId(), null, null);
for (GetItemResponse.ItemOptionDto responseItemOption : getItemResponse.getItemOptions()) {
if (responseItemOption.getId().equals(orderItemOption.getItemOptionId())) {
orderItemOptionDto = new OrderItemOptionDto(orderItemOption.getId(), responseItemOption.getOptionType(), responseItemOption.getName());
}
}
return orderItemOptionDto;
}).collect(Collectors.toList());
// this.orderItemOptionDtoList = getItemResponse.getItemOptions();
this.price = orderItem.getPrice();
this.count = orderItem.getCount();
}
@Getter
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class OrderItemOptionDto{
private Long id;
private OptionType optionType;
private String name;
}
}
}

View File

@@ -43,7 +43,7 @@ public class Order extends BaseEntity {
private Transaction transaction;
@BatchSize(size = 100)
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> orderItems = new ArrayList<>();
public static Order of(Long userId, Long userCouponId, Long storeId, OrderItem orderItem) {
@@ -83,6 +83,13 @@ public class Order extends BaseEntity {
return this;
}
public Order deleteOrderItem(OrderItem orderItem) {
this.orderPrice -= orderItem.getTotalPrice();
this.orderItems.remove(orderItem);
return this;
}
public void setOrderStatus(OrderStatus orderStatus){
this.orderStatus = orderStatus;
}
@@ -105,8 +112,4 @@ public class Order extends BaseEntity {
public void fail() {
this.orderStatus = OrderStatus.FAILED;
}
public void changOrderTime(LocalDateTime orderTime) {
this.orderTime = orderTime;
}
}

View File

@@ -7,5 +7,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface OrderRepository extends JpaRepository<Order, Long> {
Long countByUserIdAndOrderStatus(Long userId, OrderStatus orderStatus);
Optional<Order> findByUserIdAndOrderStatus(Long userId, OrderStatus orderStatus);
}

View File

@@ -1,11 +1,14 @@
package com.justpickup.orderservice.domain.order.repository;
import com.justpickup.orderservice.domain.order.dto.DashBoardDto;
import com.justpickup.orderservice.domain.order.dto.OrderMainResult;
import com.justpickup.orderservice.domain.order.dto.OrderSearchCondition;
import com.justpickup.orderservice.domain.order.dto.PrevOrderSearch;
import com.justpickup.orderservice.domain.order.entity.Order;
import com.justpickup.orderservice.domain.order.entity.OrderStatus;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.ConstantImpl;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.*;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
@@ -139,4 +142,75 @@ public class OrderRepositoryCustom {
}
public List<DashBoardDto.OrderPrice> salesAmountBetweenADay(Long storeId){
LocalDateTime today = LocalDateTime.now();
LocalDateTime startTime = LocalDateTime.of(today.getYear(),today.getMonth(),today.getDayOfMonth(),0,0);
return queryFactory
.select(Projections.fields(DashBoardDto.OrderPrice.class,
order.orderPrice
))
.from(order)
.where(
order.storeId.eq(storeId)
.and( order.orderTime.between(startTime,today))
).fetch();
}
public DashBoardDto.BestSellItem bestItemBetweenAWeek(Long storeId){
LocalDateTime today = LocalDateTime.now();
LocalDateTime startTime = LocalDateTime.of(today.getYear(),
today.getMonth(),
today.getDayOfMonth(),
0,0)
.minusDays(7);
return queryFactory.
select(
Projections.fields(DashBoardDto.BestSellItem.class,
orderItem.itemId.as("itemId"),
orderItem.count.sum().as("sumCounts")
)
)
.from(orderItem)
.join(orderItem.order, order)
.where(orderItem.order.storeId.eq(storeId)
.and(orderItem.order.orderTime.between(startTime,today)))
.groupBy(orderItem.itemId)
.orderBy(orderItem.count.sum().desc())
.limit(1L)
.fetchOne()
;
}
public List<DashBoardDto.SellAmountAWeek> salesAmountBetweenAWeek(Long storeId){
LocalDateTime today = LocalDateTime.now();
LocalDateTime startTime = LocalDateTime.of(today.getYear(),
today.getMonth(),
today.getDayOfMonth(),
0,0)
.minusDays(7);
DateTimeTemplate formattedDate =
Expressions.dateTimeTemplate(LocalDateTime.class,
"CAST({0} AS date) ", orderItem.order.orderTime );
return queryFactory.
select(
Projections.fields(DashBoardDto.SellAmountAWeek.class,
formattedDate.as("sellDate"),
orderItem.price.sum().multiply(orderItem.count.sum()).as("sellAmount")
)
)
.from(orderItem)
.join(orderItem.order, order)
.where(orderItem.order.storeId.eq(storeId)
.and(orderItem.order.orderTime.between(startTime,today)))
.groupBy(formattedDate)
.fetch();
}
}

View File

@@ -9,6 +9,7 @@ import org.springframework.data.domain.SliceImpl;
public interface OrderService {
OrderMainDto findOrderMain(OrderSearchCondition condition, Long userId);
DashBoardDto findDashboard(Long userId);
Page<PrevOrderDto> findPrevOrderMain(PrevOrderSearch search, Pageable pageable, Long userId);
SliceImpl<OrderHistoryDto> findOrderHistory(Pageable pageable, Long userId);
void addItemToBasket(OrderItemDto orderItemDto,Long storeId, Long userId);

View File

@@ -13,6 +13,7 @@ import com.justpickup.orderservice.domain.orderItemOption.entity.OrderItemOption
import com.justpickup.orderservice.global.client.store.*;
import com.justpickup.orderservice.global.client.user.GetCustomerResponse;
import com.justpickup.orderservice.global.client.user.UserClient;
import com.justpickup.orderservice.global.dto.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
@@ -162,7 +163,9 @@ public class OrderServiceImpl implements OrderService {
//orderItemOption Entity를 생성한다.
List<OrderItemOption> orderItemOptions = orderItemDto.getOrderItemOptionDtoList()
.stream().map(orderItemOptionDto -> OrderItemOption.of(orderItemDto.getId()))
.stream()
.filter(orderItemOptionDto -> orderItemOptionDto.getId()!=null)
.map(orderItemOptionDto -> OrderItemOption.of(orderItemOptionDto.getId()))
.collect(toList());
//orderItem을 Entity를 생성한다.
@@ -174,29 +177,40 @@ public class OrderServiceImpl implements OrderService {
//HARD_CODE
Long userCouponId=0L;
Long countByUserIdAndOrderStatus = orderRepository.countByUserIdAndOrderStatus(userId, OrderStatus.PENDING);
if(countByUserIdAndOrderStatus>=2) throw new OrderException("장바구니 데이터는 2건 이상 일 수 없습니다.");
Optional<Order> optionalOrder = orderRepository.findByUserIdAndOrderStatus(userId, OrderStatus.PENDING);
if(optionalOrder.isPresent()){
if(!optionalOrder.get().addOrderItem(orderItem)
.getStoreId().equals(storeId))
throw new OrderException("장바구니에 여러 카페의 메뉴를 담을수 없습니다.");
}else{
orderRepository.save(Order.of(userId,userCouponId,storeId,orderItem));
}
}
@Override
public FetchOrderDto fetchOrder(Long userId) {
//장바구니
Order order = orderRepositoryCustom.fetchOrderBasket(userId)
.orElseThrow(() -> new OrderException("장바구니 정보를 찾을 수 없습니다."));
// feign 통신 -> store 정보 가져옴
GetStoreResponse store = storeClient.getStore(String.valueOf(order.getStoreId())).getData();
// feign 통신 -> item, option 정보 가져옴
List<GetItemResponse> data = storeClient.getItemAndItemOptions(order.getOrderItems().stream()
.map(OrderItem::getItemId)
.filter(Objects::nonNull)
.collect(Collectors.toUnmodifiableList())
).getData();
Map<Long, GetItemResponse> itemMap = data.stream().collect(
//itemAndOptionMap
Map<Long, GetItemResponse> itemOptionMap = data.stream().collect(
Collectors.toMap(
GetItemResponse::getId
, getItemResponse -> getItemResponse
@@ -207,7 +221,7 @@ public class OrderServiceImpl implements OrderService {
List<FetchOrderDto.OrderItemDto> orderItemDtoList = order.getOrderItems()
.stream().map(orderItem ->
new FetchOrderDto.OrderItemDto(
itemMap.get(orderItem.getItemId())
itemOptionMap.get(orderItem.getItemId())
,orderItem))
.collect(Collectors.toList());
@@ -236,6 +250,25 @@ public class OrderServiceImpl implements OrderService {
order.setOrderStatus(orderStatus);
}
@Override
public DashBoardDto findDashboard(Long userId) {
Result<StoreByUserIdResponse> storeByUserId = storeClient.getStoreByUserId(userId);
Long storeId = storeByUserId.getData().getId();
// 하루 판매금액
List<DashBoardDto.OrderPrice> orderPrices = orderRepositoryCustom.salesAmountBetweenADay(storeId);
// 일주일 판매 상위메뉴
DashBoardDto.BestSellItem bestSellItem = orderRepositoryCustom.bestItemBetweenAWeek(storeId);
bestSellItem.setItemName(storeClient.getItem(bestSellItem.getItemId()).getData().getName());
// 일주일 판매금액( 일별 )
List<DashBoardDto.SellAmountAWeek> sellAmountAWeeks = orderRepositoryCustom.salesAmountBetweenAWeek(storeId);
return DashBoardDto.of(orderPrices , bestSellItem, sellAmountAWeeks);
}
@Override
public OrderDetailDto findOrderDetail(Long orderId) {
Order order = orderRepository.findById(orderId)

View File

@@ -1,9 +1,6 @@
package com.justpickup.orderservice.domain.order.web;
import com.justpickup.orderservice.domain.order.dto.OrderMainDto;
import com.justpickup.orderservice.domain.order.dto.OrderSearchCondition;
import com.justpickup.orderservice.domain.order.dto.PrevOrderDto;
import com.justpickup.orderservice.domain.order.dto.PrevOrderSearch;
import com.justpickup.orderservice.domain.order.dto.*;
import com.justpickup.orderservice.domain.order.entity.OrderStatus;
import com.justpickup.orderservice.domain.order.service.OrderService;
import com.justpickup.orderservice.domain.order.validator.PrevOrderSearchValidator;
@@ -40,6 +37,15 @@ public class OrderOwnerApiController {
private final OrderService orderService;
private final PrevOrderSearchValidator prevOrderSearchValidator;
@GetMapping("/dashboard")
public ResponseEntity<Result> dashboard( @RequestHeader(value="user-id") String userId) {
DashBoardDto dashboardDto = orderService.findDashboard(Long.valueOf(userId));
return ResponseEntity.status(HttpStatus.OK)
.body(Result.createSuccessResult(dashboardDto));
}
@GetMapping("/order-main")
public ResponseEntity<Result> orderMain(@Valid OrderSearchCondition condition,
@RequestHeader(value="user-id") String userHeader) {

View File

@@ -0,0 +1,41 @@
package com.justpickup.orderservice.domain.orderItem.service;
import com.justpickup.orderservice.domain.order.entity.Order;
import com.justpickup.orderservice.domain.order.entity.OrderStatus;
import com.justpickup.orderservice.domain.order.exception.OrderException;
import com.justpickup.orderservice.domain.order.repository.OrderRepository;
import com.justpickup.orderservice.domain.order.repository.OrderRepositoryCustom;
import com.justpickup.orderservice.domain.orderItem.entity.OrderItem;
import com.justpickup.orderservice.domain.orderItem.repository.OrderItemRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OrderItemService {
private final OrderItemRepository orderItemRepository;
private final OrderRepository orderRepository;
private final OrderRepositoryCustom orderRepositoryCustom;
@Transactional
public void deleteOrderItem(Long deleteOrderItemId, Long userId){
Order order = orderRepositoryCustom.fetchOrderBasket(userId)
.orElseThrow(() -> new OrderException("존재하지 않는 장바구니 아이템입니다."));
OrderItem orderItem = orderItemRepository.findById(deleteOrderItemId)
.orElseThrow(() -> new OrderException("존재하지 않는 장바구니 아이템입니다."));
order.deleteOrderItem(orderItem);
if(order.getOrderItems().size() ==0)
orderRepository.delete(order);
}
}

View File

@@ -0,0 +1,27 @@
package com.justpickup.orderservice.domain.orderItem.web;
import com.justpickup.orderservice.domain.orderItem.service.OrderItemService;
import com.justpickup.orderservice.global.dto.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/customer/orderItem")
public class OrderItemCustomerApiController {
private final OrderItemService orderItemService;
@DeleteMapping("/{orderItemId}")
public ResponseEntity deleteOrderItem(@PathVariable Long orderItemId,
@RequestHeader(value = "user-id") String userId){
orderItemService.deleteOrderItem(orderItemId,Long.parseLong(userId));
return ResponseEntity.status(HttpStatus.OK).body(Result.createSuccessResult(orderItemId));
}
}

View File

@@ -185,12 +185,15 @@ class OrderCustomerApiControllerTest {
new FetchOrderDto(2L,2L,12000L,"저스트카페"
,List.of(
new FetchOrderDto.OrderItemDto(1L,1L,"카페라테",
List.of(new GetItemResponse.ItemOptionDto(2L, OptionType.REQUIRED,"Hot")
,new GetItemResponse.ItemOptionDto(2L, OptionType.OTHER,"샷추카")),3000L,32L)
List.of(new FetchOrderDto.OrderItemDto.OrderItemOptionDto(2L, OptionType.REQUIRED,"Hot")
,new FetchOrderDto.OrderItemDto.OrderItemOptionDto(2L, OptionType.OTHER,"샷추카"))
,3000L
,32L)
)
);
given(orderService.fetchOrder(2L)).willReturn(fetchOrderDto);
//When

View File

@@ -320,4 +320,54 @@ class OrderOwnerApiControllerTest {
))
;
}
@Test
@DisplayName("점주 서비스 - 대쉬보드")
void findDashboard() throws Exception {
// GIVEN
given(orderService.findDashboard(1L))
.willReturn(
DashBoardDto.builder()
.salesAmount(1237801239L)
.bestSellItem(new DashBoardDto.BestSellItem(40L,"까메리카노",3217L))
.sellAmountAWeeks(
List.of(new DashBoardDto.SellAmountAWeek("2022-03-22",1235L),
new DashBoardDto.SellAmountAWeek("2022-03-23",235L),
new DashBoardDto.SellAmountAWeek("2022-03-24",2235L),
new DashBoardDto.SellAmountAWeek("2022-03-25",1635L),
new DashBoardDto.SellAmountAWeek("2022-03-26",35L),
new DashBoardDto.SellAmountAWeek("2022-03-27",635L))
)
.build()
);
// THEN
ResultActions actions = mockMvc.perform(get(url + "/dashboard")
.header("user-id", "1")
);
// WHEN
actions.andExpect(status().isOk())
.andExpect(jsonPath("code").value(Code.SUCCESS.name()))
.andDo(print())
.andDo(document("owner-findDashboard",
requestHeaders(
headerWithName("user-id").description("유저 고유번호")
),
responseFields(
fieldWithPath("code").description("결과 코드 SUCCESS/ERROR"),
fieldWithPath("message").description("메시지"),
fieldWithPath("data").description("데이터"),
fieldWithPath("data.salesAmount").description("총 판매금약"),
fieldWithPath("data.bestSellItem").description("7일간 베스트 판매 상품"),
fieldWithPath("data.bestSellItem.itemId").description("7일간 베스트 판매 상품 고유번호"),
fieldWithPath("data.bestSellItem.itemName").description("7일간 베스트 판매 상품명"),
fieldWithPath("data.bestSellItem.sumCounts").description("7일간 베스트 판매 상품판매량"),
fieldWithPath("data.sellAmountAWeeks").description("7일간 판매 통계"),
fieldWithPath("data.sellAmountAWeeks[*].sellDate").description("7일간 판매 통계날짜"),
fieldWithPath("data.sellAmountAWeeks[*].sellAmount").description("7일간 판매 통계날짜별 판매량")
)
))
;
}
}

View File

@@ -0,0 +1,81 @@
package com.justpickup.orderservice.domain.orderItem.web;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.justpickup.orderservice.config.TestConfig;
import com.justpickup.orderservice.domain.order.service.OrderService;
import com.justpickup.orderservice.domain.order.web.OrderCustomerApiController;
import com.justpickup.orderservice.domain.orderItem.service.OrderItemService;
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.http.HttpStatus;
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(OrderItemCustomerApiController.class)
@Import(TestConfig.class)
@AutoConfigureRestDocs(uriHost = "http://just-pickup.com", uriPort = 8001)
class OrderItemCustomerApiControllerTest {
@Autowired
ObjectMapper objectMapper;
@Autowired
MockMvc mockMvc;
@MockBean
OrderItemService orderItemService;
private final String url = "/api/customer/order";
@Test
@DisplayName("주문 아이템 삭제")
void deleteOrderItem() throws Exception{
//given
Long orderItemId = 2L;
//when
ResultActions resultActions = mockMvc.perform(
delete("/api/customer/orderItem/{orderItemId}",orderItemId)
.header("user-id", "2")
);
//then
resultActions
.andExpect(status().isOk())
.andDo(print())
.andDo(MockMvcRestDocumentation.document("delete-orderItem",
pathParameters(
parameterWithName("orderItemId").description("orderItem 고유번호")
),
requestHeaders(
headerWithName("user-id").description("유저 고유번호")
),
responseFields(
fieldWithPath("code").description("결과 코드 SUCCESS/ERROR"),
fieldWithPath("message").description("메시지"),
fieldWithPath("data").description("orderItem 고유번호")
)
));
}
}

View File

@@ -10,9 +10,11 @@
"dependencies": {
"@mdi/js": "^6.5.95",
"axios": "^0.26.0",
"chart.js": "^2.9.4",
"core-js": "^3.6.5",
"moment": "^2.29.1",
"vue": "^2.6.11",
"vue-chartjs": "^3.5.1",
"vue-daum-postcode": "^0.10.0",
"vue-router": "^3.2.0",
"vuedraggable": "^2.24.3",

View File

@@ -26,6 +26,9 @@ export default {
}
return axios.patch(process.env.VUE_APP_OWNER_SERVICE_BASEURL + "/order-service/order/" + orderId, body);
},
findDashboard(){
return axios.get(process.env.VUE_APP_API_URL + "/order/dashboard");
},
getOrderDetail(orderId) {
return axios.get(process.env.VUE_APP_OWNER_SERVICE_BASEURL + "/order-service/api/order-detail/" + orderId);
}

View File

@@ -64,7 +64,7 @@ export default {
addItemOption : function () {
if(!this.data) return;
this.dialog = false
this.dialog = !this.dialog
this.$emit('addItemOption',this.data,this.optionType)
}
}

View File

@@ -38,6 +38,7 @@
md="12"
>
<v-text-field
type="number"
v-model="modalData.itemPrice"
:rules="[() => !!modalData.itemPrice || 'This field is required']"
label="가격*"
@@ -65,7 +66,7 @@
>
<v-select
v-model="modalData.requiredOption"
:items="modalData.requiredOption"
:items="modalData.requiredOptionItems"
item-text="name"
item-value="id"
label="필수 옵션*"
@@ -87,7 +88,7 @@
>
<v-select
v-model="modalData.otherOption"
:items="modalData.otherOption"
:items="modalData.otherOptionItems"
item-text="name"
item-value="id"
label="기타 옵션"

View File

@@ -0,0 +1,11 @@
// CustomChart.js
import { Line, mixins } from 'vue-chartjs'
export default {
extends: Line,
mixins: [mixins.reactiveProp],
props:['chartData', 'options'],
mounted () {
this.renderChart(this.chartData, this.options)
}
}

View File

@@ -24,10 +24,16 @@ const authCheck = async function (to, from, next) {
};
const routes = [
{
path: '/order',
redirect: 'order',
path: '/',
redirect: 'dashboard',
component: DashboardLayout,
children: [
{
path: '/dashboard',
name: 'dashboard',
beforeEnter: authCheck,
component: () => import('./../views/HomeDashBoard')
},
{
path: '/category',
name: 'category',
@@ -61,7 +67,7 @@ const routes = [
]
},
{
path: '/',
path: '/login',
redirect: 'login',
component: AuthLayout,
children: [

View File

@@ -0,0 +1,171 @@
<template>
<v-container fluid>
<v-row>
<v-col
cols="12"
sm="6"
>
<v-card elevation="2" class="rounded-lg" style="border-left: 5px solid #f69653">
<v-card-title primary-title>
<div>
<div class="grey--text">{{ salesAmount.title }}</div>
</div>
<v-card-text class="d-flex justify-space-between align-center">
<h3 class="headline">{{salesAmount.data | currency}}</h3>
<v-avatar size="60" >
<v-icon
:color="salesAmount.color"
size="64"
>
mdi-currency-usd
</v-icon>
</v-avatar>
</v-card-text>
</v-card-title>
</v-card>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-card elevation="2" class="rounded-lg" style="border-left: 5px solid #ed6856">
<v-card-title primary-title>
<div>
<div class="grey--text">{{ bestSellItem.title }}</div>
</div>
<v-card-text class="d-flex justify-space-between align-center">
<h3 class="headline">{{bestSellItem.data.itemName}} {{ bestSellItem.data.sumCounts }}</h3>
<v-avatar size="60" >
<v-icon
:color="bestSellItem.color"
size="64"
>
mdi-coffee
</v-icon>
</v-avatar>
</v-card-text>
</v-card-title>
</v-card>
</v-col>
<v-col cols="12">
<v-card style="border-left: 5px solid #00a8e0">
<v-card-title>
<v-icon
:color="sellAmountAWeeks.color"
class="mr-12"
size="64"
>
mdi-currency-usd
</v-icon>
{{ sellAmountAWeeks.title }}
</v-card-title>
<v-sheet color="transparent">
<CustomChart
v-if="sellAmountAWeeks.loaded"
:chart-data="sellAmountAWeeks.data"
:options="sellAmountAWeeks.options"/>
</v-sheet>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script>
import orderApi from "@/api/order";
import CustomChart from "@/js/CustomChart";
export default {
name: "HomeDashBoard",
components:{
CustomChart
},
props: ['userInfo'],
data() {
return {
salesAmount: {
title: "금일 판매금액",
color: "#f69653",
data: {}
},
bestSellItem: {
title: "주간베스트 판매상품",
color: "#ed6856",
data: {}
},
sellAmountAWeeks: {
title: "주간 판매금액 그래프",
color: "#00a8e0",
loaded:false,
options:{
responsive: true,
maintainAspectRatio: false,
scales: {
xAxes: [{
ticks:{
fontColor : 'rgba(12, 13, 13, 1)',
fontSize : 14
},
}],
yAxes: [{
ticks: {
fontColor : 'rgba(12, 13, 13, 1)',
fontSize : 14,
userCallback:function (ele) {
return Number(ele).toFixed(0).replace(/(\d)(?=(\d{3})+(?:\.\d+)?$)/g, "$1,") + "원"
}
},
}]
}
},
data: {
labels: [],
datasets: [
]
},
},
}
},
methods: {
async getDashboardData() {
const response = await orderApi.findDashboard()
this.salesAmount.data = response.data.data.salesAmount
this.bestSellItem.data = response.data.data.bestSellItem
var saleDataset={
label: '일별 판매 금액',
backgroundColor: '#00a8e0',
borderColor: 'rgb(75, 192, 192)',
fill: false,
data: [],
}
for (const ele of response.data.data.sellAmountAWeeks) {
this.sellAmountAWeeks.data.labels.push(ele.sellDate)
saleDataset.data.push( ele.sellAmount)
}
this.sellAmountAWeeks.data.datasets.push(saleDataset)
this.sellAmountAWeeks.loaded = true
},
},
async mounted() {
this.getDashboardData()
}
}
</script>
<style scoped>
</style>

View File

@@ -1,13 +1,16 @@
<template>
<v-app id="inspire">
<side-bar :drawer="drawer" @drawerEvent="drawer = !drawer"></side-bar>
<side-bar :drawer="drawer" @drawerEvent="drawer = !drawer" :userInfo="userInfo"/>
<top-bar @drawerEvent="drawer = !drawer"
:notificationCounts="notificationCounts"/>
:notificationCounts="notificationCounts"
:userInfo="userInfo"
/>
<v-main style="background: #f5f5f540">
<v-container class="py-8, px-6" fluid>
<router-view
v-on:plusCount="notificationCounts++"
v-on:minusCount="notificationCounts--"
:userInfo="userInfo"
></router-view>
</v-container>
</v-main>
@@ -18,6 +21,7 @@
import Sidebar from './Sidebar.vue'
import Topbar from "./Topbar.vue";
import notificationApi from "@/api/notification";
import userApi from "@/api/user";
export default {
name: "DashboardLayout",
@@ -25,20 +29,27 @@ export default {
'side-bar': Sidebar,
'top-bar': Topbar
},
mounted() {
this.searchNotificationCounts();
async mounted() {
await this.searchNotificationCounts();
// 사용자 정보 가져오기
this.userInfo = await this.getUserInfo();
},
data: function() {
return {
drawer: true,
notificationCounts: 0
notificationCounts: 0,
userInfo: {},
}
},
methods: {
searchNotificationCounts: async function() {
const response = await notificationApi.countsNotification();
this.notificationCounts = response.data.data;
}
},
getUserInfo: async function() {
const response = await userApi.requestUserInfo();
return response.data.data;
},
}
}
</script>

View File

@@ -29,7 +29,7 @@ export default {
props: ["drawer"],
data() {
return {
drawer_sidebar:false,
drawer_sidebar:null,
links: [
{name: "지난 주문", url: "/prev-order", icon: "mdi-clipboard-check-outline"},
{name: "카테고리", url: "/category", icon: "mdi-shape-outline"},

View File

@@ -33,7 +33,7 @@
<v-icon>mdi-account-circle</v-icon>
</v-avatar>
</v-badge>
<span class="ml-3">{{ userName }}</span>
<span class="ml-3">{{ userInfo.name }}</span>
</v-chip>
</span>
</template>
@@ -44,7 +44,7 @@
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>{{ userName }}</v-list-item-title>
<v-list-item-title>{{ userInfo.name }}</v-list-item-title>
<v-list-item-subtitle>Logged In</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
@@ -63,27 +63,13 @@
</template>
<script>
import userApi from "../../api/user";
import authApi from "../../api/auth";
export default {
name: "Topbar",
props: ["notificationCounts"],
data() {
return {
userName: '',
};
},
async mounted() {
// 사용자 정보 가져오기
const data = await this.getUserInfo();
this.userName = data.name;
},
props: ["notificationCounts","userInfo"],
methods: {
getUserInfo: async function() {
const response = await userApi.requestUserInfo();
return response.data.data;
},
logout: async function() {
await authApi.logout();
},

View File

@@ -44,7 +44,7 @@ export default {
name: "LoginUser",
mounted() {
if (false == jwt.isExpired()) {
this.$router.push('/order');
this.$router.push('/');
}
},
data: function() {
@@ -62,7 +62,7 @@ export default {
const flag = await userApi.requestLoginUser(this.email, this.password);
if (flag) await this.$router.push('/order');
if (flag) await this.$router.push('/');
}
}
}

View File

@@ -162,7 +162,9 @@ export default {
categoryId: 0,
categoryList : [],
requiredOption : [],
otherOption : []
requiredOptionItems : [],
otherOption : [],
otherOptionItems : []
}
store.getCategoryList()
@@ -184,10 +186,14 @@ export default {
vm.modalData.itemPrice = item.price;
vm.modalData.categoryId = item.categoryId;
item.itemOptions.forEach(function(ele){
if(ele.optionType === "REQUIRED")
if(ele.optionType === "REQUIRED"){
vm.modalData.requiredOption.push(ele)
else
vm.modalData.requiredOptionItems.push(ele)
}
else{
vm.modalData.otherOption.push(ele)
vm.modalData.otherOptionItems.push(ele)
}
})
});
},
@@ -198,17 +204,46 @@ export default {
method='put'
else
method='post'
store.saveItem(method,itemData);
var requiredOption = []
for (const ele of this.modalData.requiredOption) {
if(isNaN(ele)) {
requiredOption.push(ele)
}else{
const option = this.modalData.requiredOptionItems.find(value => value.id == ele)
requiredOption.push(option)
}
}
var otherOption = []
for (const ele of this.modalData.otherOption) {
if(isNaN(ele)) {
otherOption.push(ele)
}else{
const option = this.modalData.otherOptionItems.find(value => value.id == ele)
otherOption.push(option)
}
}
this.modalData.requiredOption = requiredOption
this.modalData.otherOption = otherOption
store.saveItem(method,itemData)
},
addItemOption:function (itemOptionValue,type){
var item = {
name:itemOptionValue,
optionType:type
}
if(type ==='REQUIRED')
if(type ==='REQUIRED'){
this.modalData.requiredOption.push(item)
else
this.modalData.requiredOptionItems.push(item)
}
else{
this.modalData.otherOption.push(item)
this.modalData.otherOptionItems.push(item)
}
}
},

View File

@@ -24,7 +24,7 @@ public class ItemRepositoryCustom {
public Optional<Item> fetchItem(Long itemId){
Item fetchItem = queryFactory.selectFrom(item)
.join(item.itemOptions, itemOption).fetchJoin()
.leftJoin(item.itemOptions, itemOption).fetchJoin()
.join(item.category,category).fetchJoin()
.where(item.id.eq(itemId))
.fetchOne();

View File

@@ -89,10 +89,23 @@ public class ItemServiceImpl implements ItemService {
item.setItemNameAndPriceAndCategory(itemName,itemPrice,category);
//item에 해당하는 itemoption 전부조회
List<ItemOption> byItem = itemOptionRepository.findByItem(item);
//itemOptionDtos 없는 itemOption 전부 삭제
byItem.forEach(itemOption -> {
boolean isDeleted = true;
for ( ItemOptionDto itemOptionDto: itemOptionDtos) {
if(itemOption.getId().equals(itemOptionDto.getId())) isDeleted = false;
}
if(isDeleted) itemOptionRepository.delete(itemOption);
});
//id가 없으면 저장
itemOptionDtos
.forEach(itemOptionDto -> {
if(itemOptionDto.getId()!=null) return;
if (itemOptionRepository.existsById(itemOptionDto.getId()))
if(itemOptionDto.getId()==null)
itemOptionRepository.save(ItemOptionDto.createItemOption(itemOptionDto, item));
});
}

View File

@@ -9,4 +9,5 @@ import java.util.List;
public interface ItemOptionRepository extends JpaRepository<ItemOption,Long> {
List<ItemOption> findByItem(Item item);
}