해야 할 일
- 수량 +, - 버튼 동작
- 수량 변경에 따라 총 금액 변경
- 구매하기 버튼 동작 → 주문 확인 페이지로 이동
src/main/resources - templates - product - read.html
// + 플러스 버튼 처리
function plus() {
let $count = $('#count_of_product').text();
let $pno = $('#pno').text();
const $price = $('#price').find('span').text();
$count++;
const params = "count=" + $count + "&pno=" + $pno;
$.ajax("/product/check/stock?" + params).done(() => {
$(this).prev().text($count);
$(this).parent().next().next().find('.total_count').text($count);
$(this).parent().next().next().find('.total_price').text($count * $price);
}).fail(() => alert("더 이상 구입할 수 없습니다."));
}
// - 마이너스 버튼 처리
function minus() {
let $count = $('#count_of_product').text();
const $price = $('#price').find('span').text();
if ($count > 1) {
$count--;
$(this).next().text($count);
$(this).parent().next().next().find('.total_count').text($count);
$(this).parent().next().next().find('.total_price').text($count * $price);
}
}
$(document).ready(function() {
$('#minus').click(minus);
$('#plus').click(plus);
}
📝 $(document) 안에 이벤트 동작 넣어 주는 것 까먹지 말기!
ProductRestController
@GetMapping("/product/check/stock")
public ResponseEntity<Void> checkStock(Integer pno, Integer count) {
Boolean result = service.checkStock(pno, count);
if (result == true)
return ResponseEntity.ok(null);
return ResponseEntity.status(HttpStatus.CONFLICT).body(null);
}
ProductService
public Boolean checkStock(Integer pno, Integer count) {
return productDao.findById(pno).getStock() >= count;
}
src/main/resources - templates - product - read.html
// 구매하기 버튼 처리
function order() {
const $form = $('<form>').attr('method', 'get').attr('action', '/order/view').appendTo($('body'));
$('<input>').attr('type', 'hidden').attr('name', 'pno').val($('#pno').text()).appendTo($form);
$('<input>').attr('type', 'hidden').attr('name', 'count').val($('#count_of_product').text()).appendTo($form);
$form.submit();
}
$(document).ready(function() {
// 결제 버튼
$('#order').click(order);
}
구매하기 버튼을 누르면 주문 확인 페이지(/order/view)로 넘어간다.
해야 할 일
주문 확인 화면은 위와 같다. 새 배송지 버튼을 클릭하면 아래와 같은 새 창이 뜨게끔 되어 있다.
src/main/java - com.example.demo.entity - Order
package com.example.demo.entity;
import java.time.*;
import lombok.*;
@Data
@AllArgsConstructor
public class Order {
private Integer orderNo;
private String username;
private DeliveryStatus deliveryStatus;
private LocalDate orderday;
private Integer totalPrice;
private Integer addressNo;
}
src/main/java - com.example.demo.entity - OrderItem
package com.example.demo.entity;
import lombok.*;
@Data
@AllArgsConstructor
public class OrderItem {
private Integer orderNo;
private Integer pno;
private String vendor;
private String name;
private String imagename;
private Integer price;
private Integer count;
private Integer orderItemPrice;
}
src/main/java - com.example.demo.entity - DeliveryStatus
package com.example.demo.entity;
public enum DeliveryStatus {
PAY("결제 완료"), SHIPPING("배송 중"), CANCEL("취소"), RETURN("환불"), COMPLETE("배송 완료");
@Getter
public String korean;
DeliveryStatus(String kor) {
this.korean = kor;
}
}
src/mian/java - com.example.demo.entity - Address
package com.example.demo.entity;
import lombok.*;
import lombok.experimental.*;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain=true)
@Builder
public class Address {
private String username;
private Integer addressNo;
private String nickname;
private String zipcode;
private String address1;
private String address2;
private Boolean isDefault;
}
src/main/resources - templates - order - view.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<title>Insert title here</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/main.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/sweetalert2@11"></script>
</head>
<body>
<div id="page">
<header th:replace="/fragments/header.html">
</header>
<nav th:replace="/fragments/nav.html">
</nav>
<div id="main">
<aside th:replace="/fragments/aside.html">
</aside>
<section>
<div th:text="${order}"></div>
<div>
<button id="add_new_address">새 배송지</button>
<div id="address_area">
<!-- 등록된 배송지들을 라디오 버튼으로 출력하는 영역 -->
</div>
<hr>
<div id="address_info">
<span>배송지 : </span>
<!-- radio로 선택한 배송지 정보를 출력하는 영역 -->
<span id="forwardingAddress"></span>
</div>
<hr>
</div>
<button id="order" class="btn btn-info">결제</button>
</section>
</div>
<footer th:replace="/fragments/footer.html">
</footer>
</div>
</body>
</html>
src/main/resources - templates - order - new_address.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<title>Insert title here</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/css/main.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="/script/main.js"></script>
<body>
<h1>배송주소 추가</h1>
배송지명 : <input type="text" id="nickname" placeholder="배송지명" autofocus>
<br>
<input type="text" id="zipcode" placeholder="우편번호">
<input type="button" onclick="execDaumPostcode()" value="우편번호 찾기"><br>
<input type="text" id="roadAddress" placeholder="도로명주소">
<input type="text" id="jibunAddress" placeholder="지번주소">
<span id="guide" style="color:#999;display:none"></span>
<input type="text" id="detailAddress" placeholder="상세주소">
<br>
<button id="add">배송지 추가</button>
<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<script>
//본 예제에서는 도로명 주소 표기 방식에 대한 법령에 따라, 내려오는 데이터를 조합하여 올바른 주소를 구성하는 방법을 설명합니다.
function execDaumPostcode() {
new daum.Postcode({
oncomplete: function (data) {
let roadAddr = data.roadAddress; // 도로명 주소 변수
let extraRoadAddr = ''; // 참고 항목 변수
// 법정동명이 있을 경우 추가한다. (법정리는 제외)
// 법정동의 경우 마지막 문자가 "동/로/가"로 끝난다.
if (data.bname !== '' && /[동|로|가]$/g.test(data.bname)) {
extraRoadAddr += data.bname;
}
// 건물명이 있고, 공동주택일 경우 추가한다.
if (data.buildingName !== '' && data.apartment === 'Y') {
extraRoadAddr += (extraRoadAddr !== '' ? ', ' + data.buildingName : data.buildingName);
}
// 표시할 참고항목이 있을 경우, 괄호까지 추가한 최종 문자열을 만든다.
if (extraRoadAddr !== '') {
extraRoadAddr = ' (' + extraRoadAddr + ')';
}
// 우편번호와 주소 정보를 해당 필드에 넣는다.
document.getElementById('zipcode').value = data.zonecode;
document.getElementById("roadAddress").value = roadAddr;
document.getElementById("jibunAddress").value = data.jibunAddress;
}
}).open();
}
</script>
</body>
</html>
src/main/java - com.example.demo.dto - OrderDto
package com.example.demo.dto;
import java.util.List;
import com.example.demo.entity.OrderItem;
import lombok.*;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class OrderDto {
@Data
@AllArgsConstructor
public static class View {
private Integer totalprice;
private List<OrderItem> orderItems;
}
}
src/main/java - com.example.demo.controller.mvc - OrderController
package com.example.demo.controller.mvc;
@Secured("ROLE_USER")
@AllArgsConstructor
@Controller
public class OrderController {
@GetMapping("/order/new_address")
public void newAddress() {
}
@PostMapping("/order/view")
public ModelAndView orderView(Integer pno, Integer count, Principal principal, HttpSession session) {
OrderDto.View dto = service.orderOne(pno, count, principal.getName());
session.setAttribute("dto", dto);
return new ModelAndView("order/view").addObject("order", dto);
}
📝 주문 확인 페이지까지 왔다가 다시 뒤로 가기를 하는 경우가 생길 수도 있다. 그렇기 때문에 여기서 담기는 정보는 일단 session에 담아 준다.
address_area
와 address_info
블록에 주소를 출력할 코드 작성 (자바스크립트)view.html
function printAddresses() {
$.ajax("/address/all").done(result => {
if (result == "") {
$('#forwardingAddress').text("등록된 배송지가 없습니다. 배송지를 등록해 주세요.");
$('#order').prop("disabled", true);
} else {
$("#address_area").empty();
$.each(result, function(idx, address) {
const $radio = $("<input type='radio' name='address' class='address'>").attr("data-addressNo", address.addressNo).appendTo($('#address_area'));
$("<span>").text(address.nickname).appendTo($("#address_area"));
if (address.isDefault == true) {
$radio.prop("checked", true);
$('#forwardingAddress').text("(" + address.zipcode + ")" + address.address1 + " " + address.address2);
}
})
}
})
}
$(document).ready(function() {
printAddresses();
}
address_info
다르게 출력하는 코드 작성 (자바스크립트)view.html
function changeAddress() {
const $addressNo = $(this).attr("data-addressNo");
$.ajax("/address/read/" + $addressNo).done(address => {
$('#forwardingAddress').text("(" + address.zipcode + ")" + address.address1 + " " + address.address2);
})
}
$(document).ready(function() {
$("#address_area").on("click",".address", changeAddress);
}
view.html
$(document).ready(function() {
$('#add_new_address').click(function() {
const popup = window.open('/order/new_address', '_blank', 'toolbar=yes, menubar=yes, width=700, height=500');
$(popup).on("beforeunload", printAddresses);
})
})
📝
window.open()
을 이용해서 새 창을 열 수 있다.
view.html
에 출력할 정보를 보낼 컨트롤러와 서비스, DAOsrc/main/java - com.example.demo.controller.rest - AddressRestController
package com.example.demo.controller.rest;
@Secured("ROLE_USER")
@AllArgsConstructor
@RestController
public class AddressRestController {
private AddressService service;
// 새 배송지 저장
@PostMapping("/address/new")
public ResponseEntity<Void> add(Address address, Principal principal) {
Boolean result = service.save(address, principal.getName());
if (result == true)
return ResponseEntity.ok(null);
return ResponseEntity.status(HttpStatus.CONFLICT).body(null);
}
// 저장된 주소들이 있는지 조회
@GetMapping("/address/all")
public ResponseEntity<List<Address>> read(Principal principal) {
List<Address> list = service.read(principal.getName());
return ResponseEntity.ok(list);
}
// 선택된 라디오 버튼의 주소 조회
@GetMapping("/address/read/{addressNo}")
public ResponseEntity<Address> read(@PathVariable Integer addressNo, Principal principal) {
Address address = service.readOne(addressNo, principal.getName());
return ResponseEntity.ok(address);
}
}
src/main/java - com.example.demo.service - AddressService
package com.example.demo.service;
@AllArgsConstructor
@Service
public class AddressService {
private AddressDao dao;
public Boolean save(Address address, String loginId) {
address.setUsername(loginId);
if (dao.existsByUsername(loginId) == false)
address.setIsDefault(true);
return dao.save(address) == 1;
}
public List<Address> read(String loginId) {
return dao.findByUsername(loginId);
}
public Address readOne(Integer addressNo, String username) {
return dao.findByUsernameAndAddressNo(username, addressNo);
}
}
src/main/java - com.example.demo.dao - AddressDao
package com.example.demo.dao;
public interface AddressDao {
@Select("select * from address where username=#{username}")
public List<Address> findByUsername(String loginId);
@SelectKey(statement = "select address_seq.nextval from dual", keyProperty = "addressNo", before = true, resultType = Integer.class)
@Insert("insert into address values(#{username}, #{addressNo}, #{nickname}, #{zipcode}, #{address1}, #{address2}, #{isDefault})")
public Integer save(Address address);
@Select("select count(*) from address where username=#{username} and rownum=1")
public boolean existsByUsername(String loginId);
@Select("select * from address where username=#{username} and addressNo=#{addressNo}")
public Address findByUsernameAndAddressNo(String username, Integer addressNo);
}