본문 바로가기
웹개발/DIOS

[웹개발 - DIOS] 장바구니 페이지 (2)

by 오엥?은 2023. 2. 25.
반응형

⌦  장바구니 페이지


☑︎  체크박스 기능

- JavaScript

// 체크박스 전체선택 / 전체해제
function selectAll(selectAll) {
    const checkboxes = document.getElementsByName('select');

    checkboxes.forEach((checkbox) => {
        checkbox.checked = selectAll.checked;
    })
}

// 체크박스 하나라도 해제되면 전체선택 체크박스 풀리는 거
function checkSelectAll(check) {
    // 전체 체크박스
    const checkboxes
        = document.querySelectorAll('input[name="select"]');
    // 선택된 체크박스
    const checked
        = document.querySelectorAll('input[name="select"]:checked');
    // select all 체크박스
    const selectAll
        = document.querySelector('input[name="select"]');

    if (checkboxes.length === checked.length) {
        selectAll.checked = true;
    } else {
        selectAll.checked = false;
    }

}

맨 위에 있는 체크박스를 체크하면 전체 선택 / 전체 해제가 되어야 한다. 전체 선택이 되었을 때는 개별 체크박스를 하나라도 해제하면 맨 위에 있는 체크박스는 해제되어야 한다. 그에 대한 조건은 checkboxes.length === checked.length 로 했다. select 라는 name 을 가진 체크박스의 개수와 체크된 체크박스의 개수를 비교하여 같으면 체크, 다르면 체크 해제를 하면 된다.

 


☑︎  상품을 삭제하는 기능

요가매트 사봤자 집에서 요가 절대 안하기 때문에 안 살란다. 체크박스 체크하고 선택상품 삭제를 했다.

 

- JavaScript

cancelButton.addEventListener('click', e => {
    e.preventDefault();

    const items = document.querySelectorAll('[rel="line"]');

    let itemsArray = [];
    for (let item of items) {
        if (item.querySelector('[rel="checkbox"]').checked) {
            itemsArray.push({
                index: item.dataset.index
            });
        }
    }
    const xhr = new XMLHttpRequest();
    const formData = new FormData();
    formData.append('index', JSON.stringify(itemsArray));

    xhr.open('DELETE', './cart');
    xhr.onreadystatechange = () => {
        if (xhr.readyState === XMLHttpRequest.DONE) {
            if (xhr.status >= 200 && xhr.status < 300) {
                const responseObject = JSON.parse(xhr.responseText);
                switch (responseObject['result']) {
                    case 'success':
                        loadCart();
                        break;
                    default:
                        alert('잠시후 다시 시도해 주세요.')
                }
            } else {
                alert('서버와 통신하지 못하였습니다.\n\n잠시 후 다시 시도해 주세요.');
            }
        }
    };
    xhr.send(formData);
});

선택상품 삭제 버튼은 누르면 체크박스가 체크된 상품을 삭제하는 기능이다. 버튼을 눌렀을 때, 자바스크립트에서 반복문으로 작성된 상품목록 중 체크 된 것을 반복문으로 찾아낸다. 체크 된 상품의 dataset.index 정보를 반복문을 돌릴 때 마다 FormData 로 보내는 건 너무너무 안 좋은 방법이다. FormData 를 한 번에 보낼 수 있게 itemsArray 라는 빈 배열을 만든 뒤, 상품의 정보들을 담아준다.

 

✔️ JSON.stringify()

: JSON.stringify() 는 자바스크립트의 값을 JSON 문자열로 변환한다.

위와 같이 () 안에 배열이 들어간다면 그 배열 자체가 String 이 된다. 예를 들어서 JSON.stringify([1, 2, 3]) 을 하면 "[1, 2, 3]" 이 되는 것이다.

 

배열 itmesArray 에 담아준 상품의 index(삭제할 상품의 index)들을 FormData 에 담아준다. FormData 의 value 에는 객체나 배열같은 복잡한 데이터는 넣을 수 없고, 문자열만 넣을 수 있기 때문에 배열 itmesArray 를 JSON.stringify() 를 통해 문자열로 바꿔서 넣어줬다.

 

- Controller

@DeleteMapping(value = "cart",
        produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String deleteCart(@SessionAttribute(value = "user", required = false) UserEntity user,
                         @RequestParam(value = "index") String indexStr) throws JsonProcessingException {

    CartEntity[] carts = new ObjectMapper().readValue(indexStr, CartEntity[].class);

    Enum<?> result = this.storeService.deleteCart(user, carts);
    JSONObject responseObject = new JSONObject();

    responseObject.put("result", result.name().toLowerCase());

    return responseObject.toString();
}

Controller 에서 문자열로 바꾼 itmesArray 배열을 indexStr 로 가져온다.

 

✔️ ObjectMapper().readValue()

: JSON 을 Java 객체로 변환할 때 쓰는 방법

 

문자열로 받아온 indexStr 을 CartEntity 타입으로 바꿔주기 위해 ObjectMapper().readValue() 을 사용했다. CartEntity[] carts = new ObjectMapper().readValue(indexStr, CartEntity[].class); 이렇게 적어주면 indexStr 을 CartEntity 배열 타입으로 바꿔준다. 배열에는 삭제할 상품의 index 값이 들어있기 때문에 쿼리는 index 와 페이지의 session user 를 기준으로 짜면 된다.

 

 

- Service

public Enum<? extends IResult> deleteCart(UserEntity user, CartEntity[] carts) {

    int count = 0;
    for (CartEntity cart : carts) {
        count += this.storeMapper.deleteCartByIndex(user.getEmail(), cart.getIndex());
    }

    return count == carts.length
            ? CommonResult.SUCCESS
            : CommonResult.FAILURE;
}

배열을 반복문으로 돌려서 delete를 한다. 배열에 담긴 상품이 모두 삭제가 되면 SUCCESS 를 반환한다.

 

 

- Mapper, xml

int deleteCartByIndex(@Param(value = "userEmail") String userEmail,
                      @Param(value = "index") int index);
<delete id="deleteCartByIndex">
    DELETE
    FROM `dios_store`.`carts`
    WHERE `index` = #{index} AND `user_email` = #{userEmail}
    LIMIT 1
</delete>

성공하면 loadCart() 함수를 실행하는 것으로 삭제는 끝난다. 

 

여러 데이터를 배열에 담아서 삭제하는 것은 처음해 본 거라서 시행착오가 많았다. JSON.stringify() 와 ObjectMapper().readValue() 를 사용하니 쉽게 해결이 되었다. 삭제기능을 만들면서 새로운 기능을 많이 써봐서 장바구니 기능 중에 가장 재밌었당 😎


☑︎  상품을 주문하는 기능

• 상품 선택주문

• 상품 전체주문

 

- JavaScript

// 선택주문
selectOrder.addEventListener('click', e => {
    e.preventDefault();

    const items = document.querySelectorAll('[rel="line"]');

    let itemsArray = [];
    for (let item of items) {
        if (item.querySelector('[rel="checkbox"]').checked) {
            itemsArray.push({
                cartIndex: item.dataset.index,
                itemIndex: item.dataset.itemIndex,
                count: item.dataset.count,
                orderColor: item.dataset.orderColor,
                orderSize: item.dataset.orderSize
            });
        }
    }
    const formData = new FormData();
    formData.append('order', JSON.stringify(itemsArray));
    const xhr = new XMLHttpRequest();
    xhr.open('POST', './order');
    xhr.onreadystatechange = () => {
        if (xhr.readyState === XMLHttpRequest.DONE) {
            if (xhr.status >= 200 && xhr.status < 300) {
                const responseObject = JSON.parse(xhr.responseText);
                switch (responseObject['result']) {
                    case 'success':
                        window.location.href = `./order?num=${responseObject['orderNum']}`;
                        break;
                    case 'out_of_range':
                        alert('주문할 상품이 없습니다!');
                        break;
                    default:
                        alert('잠시후 다시 시도해 주세요.')
                }
            } else {
                alert('서버와 통신하지 못하였습니다.\n\n잠시 후 다시 시도해 주세요.');
            }
        }
    };
    xhr.send(formData);
});
// 전체 주문
orderAll.addEventListener('click', e => {
    e.preventDefault();

    const items = document.querySelectorAll('[rel="line"]');

    let itemsArray = [];
    for (let item of items) {
        itemsArray.push({
            cartIndex: item.dataset.index,
            itemIndex: item.dataset.itemIndex,
            count: item.dataset.count,
            orderColor: item.dataset.orderColor,
            orderSize: item.dataset.orderSize
        });
    }
    const formData = new FormData();
    formData.append('order', JSON.stringify(itemsArray));
    const xhr = new XMLHttpRequest();
    xhr.open('POST', './order');
    xhr.onreadystatechange = () => {
        if (xhr.readyState === XMLHttpRequest.DONE) {
            if (xhr.status >= 200 && xhr.status < 300) {
                const responseObject = JSON.parse(xhr.responseText);
                switch (responseObject['result']) {
                    case 'success':
                        window.location.href = `./order?num=${responseObject['orderNum']}`;
                        break;
                    case 'out_of_range':
                        alert('주문할 상품이 없습니다!');
                        break;
                    default:
                        alert('잠시후 다시 시도해 주세요.')
                }
            } else {
                alert('서버와 통신하지 못하였습니다.\n\n잠시 후 다시 시도해 주세요.');
            }
        }
    };
    xhr.send(formData);
});

장바구니 페이지에는 선택 상품 주문과 전체 상품 주문 기능이 있다.

선택 상품 주문버튼을 클릭하면 반복문을 통해 체크박스에 체크가 된 상품들을 찾는다. 그 상품들의 dataset 에 담긴 정보만 itemsArray 에 넣는다. 이것 역시 상품 삭제와 마찬가지로 FormData 에 담을 때 배열 자체를 JSON.stringify()을 통해 문자열로 바꾼 뒤 담는다.

처음엔 carts table 과 orders table 을 따로 만들지 않고 하나의 table 로 사용하려 했다. 그런데 하나로 만들게 되면 주문 완료하여 지워진 상품의 레코드를 더 이상 select 하지 못하게 되어 주문 내역 페이지를 만들 수 없다는 점이 문제가 되었다(주문이 완료가 되면 장바구니에서는 지워져야 하는데 장바구니 select 부분은 table 의 모든 레코드를 select 하므로 레코드가 영구 삭제되어야만 한다.). 그래서 carts table 과 orders table 을 따로 만들어 정보를 저장하기로 했다. orders table 에 대한 자세한 설명은 주문 페이지에서 다뤘다.

 

2023.02.25 - [웹개발] - [웹개발 - DIOS] 주문 페이지

 

[웹개발 - DIOS] 주문 페이지

⌦ 주문 페이지 ☑︎ orders table (DB) 장바구니 페이지(2) 에서도 다루었지만 orders table 을 따로 만든 이유는 하나로 만들게 되면 주문 완료하여 지워진 상품의 레코드를 더 이상 select 하지 못하게

pickmeplease.tistory.com

 

장바구니 페이지에서 주문 페이지로 넘어갈 때 orders table 에 insert 되어야 하는 정보들은 다음과 같다.

 

1. user_email (사용자 이메일)

2. cart_index (장바구니에 담긴 상품의 인덱스)

3. order_num (주문 번호)

4. count (상품의 수량)

5. item_index (상품 자체의 인덱스(items table 의 index))

6. order_color (상품 색깔)

7. order_size (상품 사이즈)

8. price (상품의 개별 가격)

9. order_status (주문 상태)

 

이 중, cart_index, count, item_index, order_color, order_size 에 들어갈 정보는 itemsArray 에 들어있다. 나머지는 Service 에서 처리해 주도록 코드를 짰다.

 

 

- Controller

@PostMapping(value = "order",
            produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public String postOrder(@SessionAttribute(value = "user", required = false) UserEntity user,
                            @RequestParam(value = "order") String orderStr) throws JsonProcessingException {

        OrderEntity[] orders = new ObjectMapper().readValue(orderStr, OrderEntity[].class);
        Enum<?> result;
        result = this.storeService.addOrders(user, orders);

        JSONObject responseObject = new JSONObject();

        responseObject.put("result", result.name().toLowerCase());
        responseObject.put("orderNum", orders[0].getOrderNum());

        return responseObject.toString();
    }

자바스크립트에서 받아온 itemsArray 를 String 타입의 orderStr 로 받았다. 마찬가지로 문자열로 이루어진 배열 orderStr 을  ObjectMapper().readValue() 를 이용하여 OrderEntity[] 타입으로 바꿔줬다. 

그리고 JSONObject 에 "orderNum" 이 키 값인 orders[0].getOrderNum() 값을 넣어준다. orderNum 은 주문번호인데, Service 에서 생성된 주문번호를 기준으로 주문 페이지 내용을 출력할 거라서 URL 에 사용하려고 넣었다. orders 배열에 담긴 첫 번째 주문번호를 JSONObject 에 넣는다. 사실 이 배열에 들어있는 주문번호는 다 똑같이 넘어오기 때문에 (같이 장바구니에 담겨있던 상품을 주문한 것이라서) 배열의 맨 마지막 주문번호를 가져와도 똑같긴하다.

 

 

- Service

public Enum<? extends IResult> addOrders(UserEntity user, OrderEntity[] orders) {
    BigInteger orderNum = new BigInteger(RandomStringUtils.randomNumeric(5) + Math.abs(user.getEmail().hashCode()));

    int count = 0;

    for (OrderEntity order : orders) {
        ItemEntity item = this.goodsMapper.selectItemByIndex(order.getItemIndex());
        order.setOrderStatus(0);
        order.setUserEmail(user.getEmail());
        order.setOrderNum(orderNum);
        order.setPrice(item.getPrice());

        // orders 의 배열의 길이만큼 상품이 insert 되면 성공
        count += this.storeMapper.insertOrder(order);
    }

    return count == orders.length
            ? CommonResult.SUCCESS
            : CommonResult.FAILURE;
}

주문번호는 주문 내역, 배송 상태를 불러올 때 필요해서 만들었다. 주문 페이지에서 결제할 상품의 정보를 불러올 때도 사용되기 때문에 유추가 가능하면 안 된다. 그래서 랜덤 숫자 5자리 + 사용자 이메일을 hashCode 로 변환한 숫자 5자리로 주문 번호를 만들었다.

 

배열에 담긴 item_index 를 통해 상품 자체의 인덱스를 select 하여 items table 에 담긴 상품의 price(가격)을 가져온다.

user_email order_num 에는 각각 장바구니 session user 와 위에서 만들어 둔 주문 번호가 들어간다.

 

window.location.href = `./order?num=${responseObject['orderNum']}`;

성공하면 주문 페이지로 넘어간다.

반응형