⌦ 주문 페이지
☑︎ orders table (DB)
장바구니 페이지(2) 에서도 다루었지만 orders table 을 따로 만든 이유는 하나로 만들게 되면 주문 완료하여 지워진 상품의 레코드를 더 이상 select 하지 못하게 되어 주문 내역 페이지를 만들 수 없다는 점이 문제가 되었다(주문이 완료가 되면 장바구니에서는 지워져야 하는데 장바구니 select 부분은 table 의 모든 레코드를 select 하므로 레코드가 영구 삭제되어야만 한다.). 그래서 carts table 과 orders table 을 따로 만들어 정보를 저장하기로 했다.
주문 페이지에서 최종 주문이 완료되면 장바구니에 담겨있다가 넘어온 상품들이 삭제되어야 한다. 주문 완료가 되었는데도 장바구니에 주문 한 상품이 남아 있으면 안되기 때문이다. 그 장바구니 삭제를 쉽게 하기위해 orders table 에 cart_index 를 만들었다. cart_index 는 carts table 의 index 와 같다. 장바구니에서 주문 페이지로 넘어갈 때 장바구니의 index 를 넘겨준다. 이렇게 따로 cart_index 를 만들어 놓으면 나중에 최종 주문이 완료되었을 때, cart_index 와 carts table 의 index 가 같은 레코드를 carts table 에서 삭제하면 된다. orders table 의 레코드들은 주문내역 페이지를 위해 삭제되면 안 된다!
처음에는 단순히 cart_index 가 carts table 의 index 와 같아야 하니까 외래키 걸어야지~~ 이딴식으로 생각해 가지고 외래키를 걸어놨다. 최종 주문을 완료하고 완료된 상품을 장바구니에서 지우는 코드를 다 짜고나서 실험을 했는데 진짜 장바구니에서 지워진 거 보고 넘 기뻤다. 근데 orders table 보니까 같이 지워진 거다. 쪼끔 눈물났다. 왜 그런건지 계속 실험하면서 찾았는데 보니까 내가 외래키 걸어놔서 carts 에서 지워지니까 orders 에서도 당연히 지워지는 거였다. 당장 외래키 풀었더니 원하는 기능을 만들 수 있었다. 지금 이렇게 적으니까 별 거 아닌 거 같아 보이는데 이 사실 발견했을 때 진짜 행복했다. 담에 DB 짜라고 하면 이런 실수 안 할 수 있을 거다.🙃 히
orders table 에 insert 되는 정보들은 상품에 관한 정보, 배송지 정보이다. 상품에 관한 정보들은 장바구니 페이지에서 넘어온다. 그 때, 배송지에 관한 정보는 insert 될 수 없다. 그 정보들은 주문 페이지에서 insert 되기 때문이다. 그렇다고 그 정보들이 null 가능하게 만들어지면 주문페이지 및 배송 과정에서 문제가 생긴다. 그렇기 때문에 상품에 관한 정보들은 not null 로 만들고 배송지에 관한 정보들은 defalut "" 로 만들었다.
☑︎ 주문할 상품의 목록 출력 기능
- Controller
@GetMapping(value = "orderItem",
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public OrderVo[] getOrderItem(@SessionAttribute(value = "user", required = false) UserEntity user,
@RequestParam(value = "num") BigInteger orderNum) {
OrderVo[] orders = this.storeService.getOrders(user, orderNum);
return orders;
}
- Service
public OrderVo[] getOrders(UserEntity user, BigInteger orderNum) {
return this.storeMapper.selectOrderByEmail(user.getEmail(), orderNum);
}
- Mapper, xml
OrderVo[] selectOrderByEmail(@Param(value = "userEmail") String userEmail,
@Param(value = "orderNum") BigInteger orderNum);
<select id="selectOrderByEmail" resultType="com.blackgreen.dios.vos.store.OrderVo">
SELECT `order`.`index` AS `index`,
`order`.`user_email` AS `UserEmail`,
`order`.`user_name` AS `userName`,
`order`.`user_contact` AS `userContact`,
`order`.`user_address_postal` AS `userAddressPostal`,
`order`.`user_address_primary` AS `userAddressPrimary`,
`order`.`user_address_secondary` AS `userAddressSecondary`,
`order`.`cart_index` AS `cartIndex`,
`order`.`order_num` AS `orderNum`,
`order`.`count` AS `count`,
`order`.`item_index` AS `itemIndex`,
`order`.`order_color` AS `orderColor`,
`order`.`order_size` AS `orderSize`,
`order`.`price` AS `price`,
`order`.`order_status` AS `orderStatus`,
`order`.`message` AS `message`,
`order`.`payment_method` AS `paymentMethod`,
`order`.`order_date` AS `orderDate`,
`item`.`item_name` AS `itemName`,
`item`.`titleImage_data` AS `image`,
`item`.`titleImage_mime` AS `imageMime`,
`status`.`status` AS `status`,
`item`.`seller_index` AS `sellerIndex`,
`seller`.`store_name` AS `storeName`
FROM `dios_store`.`orders` AS `order`
LEFT JOIN `dios_store`.`items` AS `item` ON `item`.`index` = `order`.`item_index`
LEFT JOIN `dios_store`.`order_status` AS `status` ON `status`.`index` = `order`.`order_status`
LEFT JOIN `dios_store`.`seller` AS `seller` ON `item`.`seller_index` = `seller`.`index`
WHERE `user_email` = #{userEmail}
AND `order_num` = #{orderNum}
주문할 상품의 목록을 출력하는 건 장바구니 목록 출력하는 것과 방법이 같다. orderVo 도 만들어줬다. 중요한 건 이 정보를 user_email 과 order_num 을 기준으로 select 해야한다.
- JavaScript
const loadOrder = () => {
orderContainer.innerHTML = '';
let innerPrice = parseInt('0');
let innerDelivery = parseInt('0');
const xhr = new XMLHttpRequest();
xhr.open('GET', `./orderItem?num=${new URL(window.location.href).searchParams.get('num')}`);
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status >= 200 && xhr.status < 300) {
const responseArray = JSON.parse(xhr.responseText);
for (const orderObject of responseArray) {
innerDelivery = 0;
const orderHtmlText = `
<table>
<tbody>
<tr class="tableBody" rel="line">
<td class="info">
<div class="photo">
<a class="img" href="/goods/read?gid=${orderObject['itemIndex']}">
<img class="image" alt="" src="/goods/titleImage?index=${orderObject['itemIndex']}"></a>
</div>
<div class="info-content">
<div class="brand">${orderObject['storeName']}</div>
<div class="name">${orderObject['itemName']}</div>
<div class="option">옵션 | color: ${orderObject['orderColor']}, size: ${orderObject['orderSize']}</div>
</div>
</td>
<td class="price">${orderObject['price'].toLocaleString()} 원</td>
<td class="count">${orderObject['count']}</td>
</tr>
</tbody>
</table>`;
// 총 상품금액
innerPrice += orderObject['price'] * orderObject['count'];
const domParser = new DOMParser();
const dom = domParser.parseFromString(orderHtmlText, 'text/html');
const orderElement = dom.querySelector('[rel="line"]');
orderElement.dataset.cartIndex = orderObject['cartIndex'];
orderElement.dataset.orderNum = orderObject['orderNum'];
orderContainer.append(orderElement);
price.innerText = innerPrice.toLocaleString() + " 원";
delivery.innerText = (innerDelivery).toLocaleString() + " 원";
priceAll.innerText = (innerPrice + innerDelivery).toLocaleString() + " 원";
}
}
}
};
xhr.send();
}
역시 프론트 부분은 HTML로 작성한 뒤, 반복해야 할 부분을 orderHtmlText 에 넣었다. table 의 tr 부분이 반복되는 거니까 <table><tbody></tbody></table> 로 감싸주는 거 까먹으면 안 된다. select 해 온 정보들 출력하기 위해 백틱(``) 과 ${} 을 활용한다.
총 상품금액도 주문 페이지에서는 변할 일이 없기 때문에 장바구니보다 수월하다. orderObject['price'] * orderObject['count']; 즉 가격 * 수량을 반복문이 돌아갈 때 마다 더해주면 총 금액이 나온다. 그거 그냥 innerText 하면 된다.
Dataset 에 담아줄 정보는 cartIndex(장바구니에 담긴 상품 인덱스) 와 orderNum(주문번호) 이다. cartIndex 는 주문완료 되었을 때, 장바구니에서 주문 상품 정보를 지워야하기 때문에 필요하고, orderNum 은 주문 번호를 기준으로 주문 완료된 레코드들의 status 가 배송 전, 또는 배송 완료로 바뀌어야 하기 때문이다.
☑︎ '주문자 정보와 동일' 기능
- Controller
@RequestMapping(value = "order",
method = RequestMethod.GET)
public ModelAndView getOrder(@SessionAttribute(value = "user", required = false) UserEntity user) {
ModelAndView modelAndView;
if (user == null) {
modelAndView = new ModelAndView("redirect:/dios/login");
} else {
modelAndView = new ModelAndView("store/order");
}
LocalDateTime date = LocalDateTime.now();
date = date.plusDays(2);
String nowDate = date.format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
modelAndView.addObject("user", user);
// 입금기한 : 2일으로 줌
modelAndView.addObject("date", nowDate);
return modelAndView;
}
Controller 에서 주문 페이지의 session user 를 받아와서 modelAndView 에 추가해줬다. 이 정보는 HTML 에서 사용한다.
- HTML
<tr class="contact">
<th class="contact-container">연락처</th>
<td class="userContact">
<a class="redStar">*</a>
<input hidden rel="contact" type="text" th:value="${user.getContact()}">
<input class="input" type="text" rel="contactText">
<a class="contactText">'-' 를 제외하고 입력해 주세요.</a>
</td>
</tr>
배송지 정보 중, 연락처 부분의 HTML 이다. 연락처 뿐만 아니라 배송지 정보 모든 요소에 타임리프를 사용한 value 값을 넣어준다. 그리고 이 input 태그에는 hidden 을 줘서 사용자는 볼 수 없게 만들었다.
- JavaScript
function check(box) {
const nameText = document.querySelector('[rel="nameText"]');
const contactText = document.querySelector('[rel="contactText"]');
const addressPostalText = document.querySelector('[rel="addressPostalText"]');
const addressPrimaryText = document.querySelector('[rel="addressPrimaryText"]');
const addressSecondaryText = document.querySelector('[rel="addressSecondaryText"]');
if (box.checked === true) {
nameText.value = document.querySelector('[rel="name"]').value;
contactText.value = document.querySelector('[rel="contact"]').value;
addressPostalText.value = document.querySelector('[rel="addressPostal"]').value;
addressPrimaryText.value = document.querySelector('[rel="addressPrimary"]').value;
addressSecondaryText.value = document.querySelector('[rel="addressSecondary"]').value;
} else if (box.checked === false) {
nameText.value = '';
contactText.value = '';
addressPostalText.value = '';
addressPrimaryText.value = '';
addressSecondaryText.value = '';
}
}
'주문자 정보와 동일' 체크박스를 체크했을 때, 사용자가 정보를 입력할 수 있는 input text 의 value 를 사용자 정보를 넣고 숨겨뒀던 input 의 value 로 바꿔줬다. 만약에 체크가 해제되면, 빈 값('') 으로 바꿨다.
- HTML
<input class="input check" type="checkbox" rel="sameButton" onclick="check(this)">
<a class="checkText">주문자 정보와 동일</a>
자바스크립트에서 만들어 준 함수를 HTML 체크박스 input 태그에 onclick="check(this)" 의 형태로 적어준다.
☑︎ 주소찾기 기능
- HTML (header)
<script src="https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
- JavaScript
const findButton = document.querySelector('[rel="find"]');
findButton.addEventListener('click', () => {
new daum.Postcode({
oncomplete: e => {
document.querySelector('[rel="addressFindPanel"]').classList.remove('visible');
document.querySelector('[rel="addressPostalText"]').value = e['zonecode'];
document.querySelector('[rel="addressPrimaryText"]').value = e['address'];
document.querySelector('[rel="addressSecondaryText"]').value = '';
document.querySelector('[rel="addressSecondaryText"]').focus();
}
}).embed(form.querySelector('[rel="addressFindPanelDialog"]'));
form.querySelector('[rel="addressFindPanel"]').classList.add('visible');
});
form.querySelector('[rel="addressFindPanel"]').addEventListener('click', () => {
form.querySelector('[rel="addressFindPanel"]').classList.remove('visible');
});
주소 찾는 기능은 카카오(Daum) 주소찾기 API 를 사용했다. 사용법 완전 쉽다. HTML 의 헤더에 위 내용 붙여넣고 우편번호 'zonecode', 주소 'address' 선택하여 value 값으로 넣으면 된다.
☑︎ 주문 기능
결제하기 버튼을 누르면 주문이 최종 완료가 된 것이기 때문에, 주문이 완료된 상품들의 정보는 장바구니에서 지워져야 한다. 그와 동시에 배송지 정보, 결제 정보는 update 되어야 한다.
- Controller
@PatchMapping(value = "orderSuccess",
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String patchOrderSuccess(@SessionAttribute(value = "user", required = false) UserEntity user,
OrderEntity order) {
Enum<?> result = this.storeService.executeOrder(user, order);
JSONObject responseObject = new JSONObject();
responseObject.put("result", result.name().toLowerCase());
return responseObject.toString();
}
- Service
public Enum<? extends IResult> executeOrder(UserEntity user, OrderEntity orderInfo) {
if (user == null) {
return CommonResult.FAILURE;
}
int count = 0;
OrderEntity[] orders = this.storeMapper.selectOrderByOrderNum(orderInfo.getOrderNum());
for (OrderEntity order : orders) {
// 삭제
count += this.storeMapper.deleteCartByIndex(user.getEmail(), order.getCartIndex());
// 정보수정
order.setUserName(orderInfo.getUserName());
order.setUserContact(orderInfo.getUserContact());
order.setUserAddressPostal(orderInfo.getUserAddressPostal());
order.setUserAddressPrimary(orderInfo.getUserAddressPrimary());
order.setUserAddressSecondary(orderInfo.getUserAddressSecondary());
order.setMessage(orderInfo.getMessage());
order.setPaymentMethod(orderInfo.getPaymentMethod());
order.setOrderStatus(1);
count += this.storeMapper.updateOrder(order);
}
return count == orders.length * 2 // 카트와 정보수정이 동시에 일어나야돼서 곱하기 2를 해준다.
? CommonResult.SUCCESS
: CommonResult.FAILURE;
}
일단 주문번호를 기준으로 레코드를 select 한다. 주문 상품은 한 개 이상이니까 배열로 받는다. 그리고 반복문을 돌리면서 주문한 사람의 email, 주문한 상품의 장바구니 index 를 기준으로 carts table 에 있는 레코드들을 삭제한다. 그와 동시에 배송지 정보들을 insert 한다.
- JavaScript
window.location.href = `./orderSuccess?num=` + new URL(window.location.href).searchParams.get('num');
성공하면 주문 완료 페이지로 넘어간다.
시간 상의 문제로 결제 API 는 넣지 못한 게 너무 아쉽다. 다음에 기회가 되면 결제 API 도 사용해 보고싶다. 💶
'웹개발 > DIOS' 카테고리의 다른 글
[웹개발 - DIOS] 랜덤으로 상품 추천하기 (0) | 2023.02.26 |
---|---|
[웹개발 - DIOS] 주문완료 페이지 (0) | 2023.02.25 |
[웹개발 - DIOS] 장바구니 페이지 (2) (0) | 2023.02.25 |
[웹개발 - DIOS] 장바구니 페이지 (1) (0) | 2023.02.24 |
[웹개발 - DIOS] 기록장 페이지 (0) | 2023.02.23 |