본문 바로가기
웹개발/DIOS

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

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

⌦  장바구니 페이지


☑︎  장바구니 상품 목록을 출력하는 기능

carts table 을 몇 번을 고쳤는지 모르겠다. items table 과 관련이 있기 때문에 items table 만드는 것부터 items 파트 담당 팀원과 같이 머리 싸매서 만들었다. 이 프로젝트 하면서 가장 머리통 깨질 뻔 한 일이다. 제일 효율적인 table 을 만들기 위해 에러도 많이 내고 여러가지 방법으로 바꿔가면서 만들었다. 

 

 

- CartVo

public class CartVo extends CartEntity {

    private String itemName;
    private int price;
    private byte[] image;
    private String imageMime;
    
}

장바구니 페이지에서 필요한 정보는 carts table 에 들어있는 정보보다 많기 때문에 CartEntity 를 extends 하는 CartVo 를 만들었다. 그리고 상품이름, 상품 가격, 상품 사진에 관한 정보를 추가했다.

 

- Controller

@GetMapping(value = "cartItem", produces = MediaType.TEXT_HTML_VALUE)
@ResponseBody
public String getCartItem(@SessionAttribute(value = "user", required = false) UserEntity user) {

    JSONArray responseArray = new JSONArray();

    CartVo[] carts = this.storeService.getCarts(user);

    for (CartVo cart : carts) {
        JSONObject cartObject = new JSONObject();

        cartObject.put("index", cart.getIndex());
        cartObject.put("userEmail", cart.getUserEmail());
        cartObject.put("count", cart.getCount());
        cartObject.put("itemIndex", cart.getItemIndex());
        cartObject.put("orderColor", cart.getOrderColor());
        cartObject.put("orderSize", cart.getOrderSize());
        cartObject.put("itemName", cart.getItemName());
        cartObject.put("price", cart.getPrice());
        cartObject.put("image", cart.getImage());

        responseArray.put(cartObject);
    }
    
    return responseArray.toString();
}

- Service

public CartVo[] getCarts(UserEntity user) {

    return this.storeMapper.selectCartByEmail(user.getEmail());
}

- Mapper, xml

CartVo[] selectCartByEmail(@Param(value = "userEmail") String userEmail);
<select id="selectCartByEmail" resultType="com.blackgreen.dios.vos.store.CartVo">
    SELECT `cart`.`index`           AS `index`,
           `cart`.`user_email`      AS `UserEmail`,
           `cart`.`count`           AS `count`,
           `cart`.`item_index`      AS `itemIndex`,
           `cart`.`order_color`     AS `orderColor`,
           `cart`.`order_size`      AS `orderSize`,
           `item`.`item_name`       AS `itemName`,
           `item`.`price`           AS `price`,
           `item`.`titleImage_data` AS `image`,
           `item`.`titleImage_mime` AS `imageMime`
    FROM `dios_store`.`carts` AS `cart`
             LEFT JOIN `dios_store`.`items` AS `item` ON `item`.`index` = `cart`.`item_index`
    WHERE `user_email` = #{userEmail}
</select>

장바구니에 담긴 상품들은 모두 select 되어야 하고, 목록으로 보여야 한다. 그러기 위해서 CartVo 에 있는 요소들을 배열로 담은 뒤 반복문으로 출력하는 방법을 사용했다. CartVo 는 carts table 에 items table 이 LEFT JOIN 해서 채워진다.

 

Controller 에서 session uesr(email) 을 기준으로 select 한 record 들을 carts 라는 CartVo[] 타입의 변수에 담는다. 그리고 반복문을 통해 이 record 들을 JSONObject 에 담고, 한 바퀴 돌 때 마다 JSONArray 에 담았다. 그러면 [{"키" : "값"}, {"키" : "값"}, {"키" : "값"}, {"키" : "값"}, ... ] 의 형태로 record 들이 담기게 된다.

 

- JavaScript

JavaScript 에 loadCart 함수를 만든다. loadCart 는 장바구니에 담긴 상품 목록을 출력하는 함수이다.

 

일단 HTML, CSS 로 장바구니 목록의 프론트 부분을 작성한 뒤, 상품 하나하나의 정보를 담고 있으며 상품이 추가될 때 마다 반복되어야 할 부분을 JavaScript 로 가지고 와서 cartHtmlText 에 넣는다.

반복되어야 할 부분

✔️ Template literals

: 템플릿 리터럴은 내장된 표현식을 허용하는 문자열 리터럴이다. 여러 줄로 이뤄진 문자열과 문자 보간기능을 사용할 수 있다. 템플릿 리터럴은 이중 따옴표 나 작은 따옴표 대신 백틱(` `) 을 이용한다. 템플릿 리터럴은 또한 플레이스 홀더를 이용하여 표현식을 넣을 수 있는데, 이는 $와 중괄호( $ {expression} ) 로 표기할 수 있다.

 

✔️ JSON.parse() (파싱)

:JSON은 웹 서버와 데이터를 교환하는데 주로 사용된다. 웹 서버에서 데이터를 수신할 때 데이터는 항상 문자열이다. ​이 데이터를 JSON.parse()를 사용해서 파싱하면 데이터는 자바스크립트 객체가 된다.

예를 들어 var obj = JSON.parse('{"키1" : "값1", "키2" : "값2", "키3" : "값3"}') 을 한다면, obj.키1 = 값1 , obj.키2 = 값2, obj.키3 = 값3 이 되는 것이다.

 

Controller 에서 배열으로 담아줬던 장바구니 상품 정보들을 반복문을 사용하여 템플릿 리터럴인 cartHtmlText 의 ${} 안에 넣어준다.

나는 상품 목록을 table 로 만들었기 때문에 반복되는 부분은 tboby 부분이었다. 처음엔 cartHtmlText 안에 <tr></tr> 내용만 넣었는데 출력이 되지 않았다. 알고보니 tr 은 table > tbody 안의 요소이기 때문에 <table><tbody></tbody></table> 로 감싸줘야 했다.

 

이 반복문이 전부 돌면 DB 안의 장바구니 상품을 모두 출력할 수 있다. 그런데 함수가 실행될 때 마다 초기화를 해주지 않으면 똑같은 내용이 엄청나게 반복될 수가 있다. 그래서 cartContainer.innerHTML = ''; 를 꼭 써 줘야 한다.

 

const domParser = new DOMParser();
const dom = domParser.parseFromString(cartHtmlText, 'text/html');

const cartElement = dom.querySelector('[rel="line"]');

cartContainer.append(cartElement);

✔️ DomParser

: DomParser 인터페이스는 Dom document 문서에 맞는 xml 또는 html 소스코드를 해석할 수 있는 기반을 제공한다.

 

DomParser 를 이용하여 작성한 cartHtmlText 를 html 로 해석되게 한다. cartHtmlText 의 tr 부분 (반복되는 부분) 에 rel="line" 를 추가하여 dom.querySelector('[rel="line"]'); 를 상수 cartElement 에 담았다. 그리고 그걸 cartContainer 에 추가하면 완성이다.

 

loadCart();	// loadCart 함수 실행

함수 호출하는거 까먹으면 안 된다.

 


☑︎  상품의 수량을 변경하는 기능

상품 개수가 변경됨에 따라 상품 개별 구매가와 총 판매가, 총 결제 금액이 바뀐다.

 

- Controller

// 수량 변경 : 더하기
@PatchMapping(value = "plusCount",
        produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String patchPlusCount(@SessionAttribute(value = "user", required = false) UserEntity user,
                             @RequestParam(value = "index") int index) {
    JSONObject responseObject = new JSONObject();
    responseObject.put("count", this.storeService.updateCountPlus(user, index));
    responseObject.put("price", this.storeService.getCartItemPrice(user, index));
    return responseObject.toString();
}

// 수량 변경 : 빼기
@PatchMapping(value = "minusCount",
        produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String patchMinusCount(@SessionAttribute(value = "user", required = false) UserEntity user,
                              @RequestParam(value = "index") int index) {

    JSONObject responseObject = new JSONObject();
    responseObject.put("count", this.storeService.updateCountMinus(user, index));
    responseObject.put("price", this.storeService.getCartItemPrice(user, index));
    return responseObject.toString();
}

- Service

// 상품수량 변경 : 더하기, 빼기
public int updateCountPlus(UserEntity user, int index) {
    CartVo cart = this.storeMapper.selectCartByIndex(user.getEmail(), index);

    int count = cart.getCount();
    cart.setCount(count + 1);
    this.storeMapper.updateCount(cart);

    return cart.getCount();
}

public int updateCountMinus(UserEntity user, int index) {
    CartVo cart = this.storeMapper.selectCartByIndex(user.getEmail(), index);

    int count = cart.getCount();
    cart.setCount(count - 1);
    this.storeMapper.updateCount(cart);

    return cart.getCount();
}

- Mapper, xml

int updateCount(CartEntity cart);
<update id="updateCount"
        parameterType="com.blackgreen.dios.entities.store.CartEntity">
    UPDATE `dios_store`.`carts`
    SET `count` = #{count}
    WHERE BINARY `index` = #{index}
    LIMIT 1;
</update>

상품의 개수를 더하거나 빼거나 둘 다 updatCount 쿼리를 사용한다. 수량이 더해지고 빼지는 건 service 에서 cart.setCount(count + 1);cart.setCount(count - 1); 로 나누어 처리해 줬다. 

 

 

- JavaScript

const cartElement = dom.querySelector('[rel="line"]');

cartElement.dataset.index = cartObject['index'];
cartElement.dataset.itemIndex = cartObject['itemIndex'];
cartElement.dataset.count = cartObject['count'];
cartElement.dataset.orderColor = cartObject['orderColor'];
cartElement.dataset.orderSize = cartObject['orderSize'];
cartElement.dataset.price = cartObject['price'];
// 수량 변경 : 더하기 버튼 눌렀을 때
const plus = dom.querySelector('[rel="plus"]');
const count =  dom.querySelector('[rel="count"]');
const price =  dom.querySelector('[rel="productPriceAll"]');

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


    const xhr = new XMLHttpRequest();
    const formData = new FormData();
    formData.append('index', cartObject['index']);

    xhr.open('PATCH', './plusCount');
    xhr.onreadystatechange = () => {
        if (xhr.readyState === XMLHttpRequest.DONE) {
            if (xhr.status >= 200 && xhr.status < 300) {
                const responseObject = JSON.parse(xhr.responseText);
                cartElement.dataset.count = responseObject['count'];
                count.innerText = responseObject['count'];
                price.innerText = (responseObject['count'] * responseObject['price']).toLocaleString() + ' 원';
                calcPrice();
            } else {
                alert('서버와 통신하지 못하였습니다.\n\n잠시 후 다시 시도해 주세요.');
            }
        }
    };
    xhr.send(formData);
});

// 수량 변경 : 빼기 버튼 눌렀을 때
const minus = dom.querySelector('[rel="minus"]');

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

    if (count === 1) {
        alert('수량은 1개 이상이어야 합니다.');
        return false;
    }

    const xhr = new XMLHttpRequest();
    const formData = new FormData();
    formData.append('index', cartObject['index']);

    xhr.open('PATCH', './minusCount');
    xhr.onreadystatechange = () => {
        if (xhr.readyState === XMLHttpRequest.DONE) {
            if (xhr.status >= 200 && xhr.status < 300) {
                const responseObject = JSON.parse(xhr.responseText);
                cartElement.dataset.count = responseObject['count'];
                count.innerText = responseObject['count'];
                price.innerText = (responseObject['count'] * responseObject['price']).toLocaleString() + ' 원';
                calcPrice();
            } else {
                alert('서버와 통신하지 못하였습니다.\n\n잠시 후 다시 시도해 주세요.');
            }
        }
    };
    xhr.send(formData);
});

✔️ Dataset

: 특정 DOM 요소에 사용자가 임의로 (key / value) 형태로 데이터를 저장 가능하다. 

dataset 자체는 읽기 전용 속성이라 별도로 재설정이 불가하고, 해당 내용을 변경하려면 dataset 소속의 keyname 에 접근하여 변경해야 한다. 예를 들면 cartElement.dataset.count = responseObject['count']; 이런 거다.

 

수량이 변경되는 것은 단순히 프론트에서만 변경되는 것이 아니라 DB 에 변경된 값이 들어가기까지 하는 것이기 때문에 loadCart() 함수 안에서 이루어져야 한다. 수량이 변경되는 것은 service 에서 처리했기 때문에 FormData 에 변경할 상품의 인덱스만 보내주면 수량은 알아서 변경된 값이 들어갈 것이다. 수량을 빼는 부분에는 클릭했을 때의 count 가 1 이면 수량은 1 이상이어야 한다는 알림이 뜨며 실행하지 못하게 코드를 작성했다.

 

수량 변경이 완료되면, dataset 을 이용하여 update 된 값으로 다시 설정한다. 장바구니 페이지 프론트 부분도 innerText 를 통해 변경된 값으로 즉시 변경한다. price 부분에 toLocaleString() 을 사용했는데, 이것은 돈 단위로 콤마(,)를 찍어준다. 마지막으로 총 결제 금액을 계산하는 함수인 calcPrice() 함수를 실행시킨다.

 

// 가격 계산하는 함수
const calcPrice = () => {
    const lines = document.querySelectorAll('[rel="line"]');
    let sum = 0;
    lines.forEach(line => {
        const checkBox = line.querySelector('[rel="checkbox"]');
        if (checkBox.checked) {
            const price = parseInt(line.dataset.price);
            const count = parseInt(line.dataset.count);
            sum += price * count;
        }
    });
    const priceAll = document.querySelector('[rel="priceAll"]');

    priceContainer.innerText = sum.toLocaleString() + '원';
    priceAll.innerText = sum.toLocaleString() + '원';
};

 

총 결제 금액은 체크된 상품만 계산하여 출력된다.

 

calcPrice 는 총 가격을 계산하는 함수이다. lines 는 반복되어 출력된 모든 상품 목록이 담긴 배열이다(querySelectorAll 을 사용하면 자동으로 배열이 생성된다.). 총 결제 금액은 체크박스가 체크된 상품만 계산이 되어야 하기 때문에 반복문을 만들어 그 안에 checkBox.checked 라는 조건을 주고 그 안에서 계산이 이루어지도록 했다. calcPrice 함수는 loadCart 안에서 만든 게 아니고 개별적으로 만들어진 함수이기 때문에 DB 의 레코드들은 dataset 으로 담긴 걸로 사용했다. 그 값들은 String 이기 때문에 parseInt 를 해줘야 계산이 가능하다. 변수 sum 을 만들어서 체크된 상품의 개수와 가격을 곱한 값을 다 더해줬다. 그 다음 innerText 를 통해 장바구니 페이지 프론트 부분 총 판매가와 총 결제 금액을 바꿔주면 된다.

 

 

 

 

 

 

반응형