[javascript] 장바구니 수량 변경 및 자동 합계 구하기

----------------------------------------------------------------------------------

2021.05.28 업데이트

소스에 체크박스 체크에 따라 값이 자동 계산되도록 개선

업/다운 화살표 아이콘을 삭제해도 기능 동작에 문제가 없도록 개선

----------------------------------------------------------------------------------

쇼핑몰 장바구니를 관리하는 프론트엔드 화면을 제작해보겠습니다.

쇼핑몰이 아니어도 견적서나, 금액 합계를 구하는데 적용할수 있는 기능으로 활용할 수 있습니다.

요청하시는 분이 있어 완성된 풀소스를 올려드립니다. 다운로드 받아 압축풀어서 html 파일을 열면 동작되는걸 확인해볼 수 있습니다.

basket.zip0.00MB

기능 구현은 객체리터럴로 합니다.

먼저 HTML 페이지 구성을 살펴보겠습니다.

내용은 길지만 장바구니 상품목록이기 때문에 반복되는 내용은 생략했습니다.

가장 중요한 부분은 "장바구니 수량 변경" 부분입니다. 입력 필드에 표시되는 갯수를 직접 변경하거나, 화살표(업/다운)로 수량을 증가/감소하는 경우 자동으로 상품 합계와 수량 합계가 반영되어 보이도록 하는 것이 목표입니다.

<form name="orderform" id="orderform" method="post" class="orderform" action="/Order">
    <div class="basket" id="basket">
        <!-- "장바구니 헤더" -->
        <div class="row head">
            <div class="check">선택</div>
            <div class="img">이미지</div>
            <div class="pname">상품명</div>
            <div class="basketprice">가격</div>
            <div class="num">수량</div>
            <div class="sum">합계</div>
            <div class="basketcmd">삭제</div>
        </div>
        <!-- "장바구니 상품 목록" -->
        <div class="row data">
            <div class="check"><input type="checkbox" name="buy" value="260" checked="">&nbsp;</div>
            <div class="img"><img src="./backup/704/img/basket1.jpg" width="60"></div>
            <div class="pname">
                <span>찜마마(XJ-92214/3)</span>
            </div>
            <div class="basketprice"><input type="hidden" name="p_price" id="p_price1" class="p_price" value="20000">20,000원</div>
            <div class="num">
                <!-- "장바구니 수량 변경" -->
                <div class="updown">
                    <input type="text" name="p_num1" id="p_num1" size="2" maxlength="4" class="p_num" value="2">
                    <span><i class="fas fa-arrow-alt-circle-up up"></i></span>
                    <span><i class="fas fa-arrow-alt-circle-down down"></i></span>
                </div>
            </div>
            <!-- "장바구니 상품 합계" -->
            <div class="sum">40,000원</div>
            <div class="basketcmd"><a href="#" class="abutton">삭제</a></div>
        </div>
    </div>
    <!-- "장바구니 기능 버튼" -->
    <div class="right-align basketrowcmd">
        <a href="#" class="abutton">선택상품삭제</a>
        <a href="#" class="abutton">장바구니비우기</a>
    </div>
    <!-- "장바구니 전체 합계 정보" -->
    <div class="bigtext right-align sumcount" id="sum_p_num">상품갯수: 4개</div>
    <div class="bigtext right-align box blue summoney" id="sum_p_price">합계금액: 74,200원</div>


    <div id="goorder" class="">
        <div class="clear"></div>
        <div class="buttongroup center-align cmd">
            <a href="#">선택한 상품 주문</a>
        </div>
    </div>
</form>

장바구니가 예쁘게 보이도록 CSS로 레이아웃을 만듭니다.

눈여겨 봐두어야 하는 것이 있는데, 테이블 구조처럼 보이지만 HTML 소스를 보면 <div></div>와 <span></span> 태그로 제작된 것입니다.

또는 리스트(<ul></ul>)로 만들 수도 있습니다.

<table></table> 태그를 사용하지 않는 것은 반응형 레이아웃을 위해서입니다.

기본적으로 테이블 태그는 반응형 레이아웃 및 모바일 화면에 적용이 쉽지 않기 때문에 반응형 레이아웃을 구현하려면 테이블 태그는 사용해서는 안됩니다.

/* 레이아웃 외곽 너비 400px 제한*/
.wrap{
    max-width: 480px;
    margin: 0 auto; /* 화면 가운데로 */
    background-color: #fff;
    height: 100%;
    padding: 20px;
    box-sizing: border-box;


}
.reviewform textarea{
    width: 100%;
    padding: 10px;
    box-sizing: border-box;
}
.rating .rate_radio {
    position: relative;
    display: inline-block;
    z-index: 20;
    opacity: 0.001;
    width: 60px;
    height: 60px;
    background-color: #fff;
    cursor: pointer;
    vertical-align: top;
    display: none;
}
.rating .rate_radio + label {
    position: relative;
    display: inline-block;
    margin-left: -4px;
    z-index: 10;
    width: 60px;
    height: 60px;
    background-image: url('./backup/704/img/starrate.png');
    background-repeat: no-repeat;
    background-size: 60px 60px;
    cursor: pointer;
    background-color: #f0f0f0;
}
.rating .rate_radio:checked + label {
    background-color: #ff8;
}


.cmd{
    margin-top: 20px;
    text-align: right;
}
.cmd input[type="button"]{
    padding: 10px 20px;
    border: 1px solid #e8e8e8;
    background-color: #fff;
    background-color:#000;  
    color: #fff;
}


.warning_msg {
    display: none;
    position: relative;
    text-align: center;
    background: #ffffff;
    line-height: 26px;
    width: 100%;
    color: red;
    padding: 10px;
    box-sizing: border-box;
    border: 1px solid #e0e0e0;
}

기본적인 장바구니 형태는 다 갖췄으므로 이제 자바스크립트로 기능 버튼들과 수량 변경에 반응하는 이벤트 리스너들을 추가합니다.

실제 동작 기능은 객체리터럴로 basket 객체에 메서드를 만들어서 구현을 하고, 여기서는 구현된 메서드가 있다고 가정하고, 이벤트 리스너를 추가합니다.

//이벤트 리스너 등록
document.addEventListener('DOMContentLoaded', function(){
    // "선택 상품 삭제" 버튼 클릭
    document.querySelector('.basketrowcmd a:first-child').addEventListener('click', function(){
        basket.delCheckedItem();
    });
    // "장바구니 비우기" 버튼 클릭
    document.querySelector('.basketrowcmd a:nth-child(2)').addEventListener('click', function(){
        basket.delAllItem();
    });
    // 장바구니 행 "삭제" 버튼 클릭
    document.querySelectorAll('.basketcmd a').forEach(
        function(item){
            item.addEventListener('click', function(){
                basket.delItem();
            });
        }
    );   
    // 수량변경 - 이벤트 델리게이션으로 이벤트 종류 구분해 처리
    document.querySelectorAll('.updown').forEach(
        function(item, idx){
            //수량 입력 필드 값 변경
            item.querySelector('input').addEventListener('keyup', function(){
                basket.changePNum(idx+1);
            });
            //수량 증가 화살표 클릭
            item.children[1].addEventListener('click', function(){
                basket.changePNum(idx+1);
            });
            //수량 감소 화살표 클릭
            item.children[2].addEventListener('click', function(){
                basket.changePNum(idx+1);
            });
        }
    );
    //앵커 # 대체해 스크롤 탑 차단
    document.querySelectorAll('a[href="#"]').forEach(function(item){
        item.setAttribute('href','javascript:void(0)');
    });
});

이제 basket 객체 리터럴을 만듭니다.

중요한 부분은 reCalc() 와 updateUI() 메서드입니다.

이벤트 리스너에서 클릭 이벤트가 발생해 수량이 변경되면, 합계를 다시 구하는 reCalc()를 호출하고, 합계 계산이 완료되면 updateUI() 메서드를 호출해 계산해서 저장한 합계 값을 화면에 반영합니다.

예제에서는 자동 합계만을 구하지만, 실제 쇼핑몰 장바구니 기능에는 더 많은 화면 업데이트 요소들이 있기 때문에 화면 갱신하는 부분만 별도의 메서드로 분리해 관리함으로써 화면 갱신을 일관되게 관리할 수 있기 때문입니다.

let basket = {
    totalCount: 0, //전체 갯수 변수
    totalPrice: 0, //전체 합계액 변수
    //체크한 장바구니 상품 비우기
    delCheckedItem: function(){
        document.querySelectorAll("input[name=buy]:checked").forEach(function (item) {
            item.parentElement.parentElement.parentElement.remove();
        });
        //AJAX 서버 업데이트 전송
    
        //전송 처리 결과가 성공이면
        this.reCalc();
        this.updateUI();
    },
    //장바구니 전체 비우기
    delAllItem: function(){
        document.querySelectorAll('.row.data').forEach(function (item) {
            item.remove();
          });
          //AJAX 서버 업데이트 전송
        
          //전송 처리 결과가 성공이면
          this.totalCount = 0;
          this.totalPrice = 0;
          this.reCalc();
          this.updateUI();
    },
    //재계산
    reCalc: function(){
        this.totalCount = 0;
        this.totalPrice = 0;
        document.querySelectorAll(".p_num").forEach(function (item) {
            var count = parseInt(item.getAttribute('value'));9999
            this.totalCount += count;
            var price = item.parentElement.parentElement.previousElementSibling.firstElementChild.getAttribute('value');
            this.totalPrice += count * price;
        }, this); // forEach 2번째 파라메터로 객체를 넘겨서 this 가 객체리터럴을 가리키도록 함. - thisArg
    },
    //화면 업데이트
    updateUI: function () {
        document.querySelector('#sum_p_num').textContent = '상품갯수: ' + this.totalCount.formatNumber() + '개';
        document.querySelector('#sum_p_price').textContent = '합계금액: ' + this.totalPrice.formatNumber() + '원';
    },
    //개별 수량 변경
    changePNum: function (pos) {
        var item = document.querySelector('input[name=p_num'+pos+']');
        var p_num = parseInt(item.getAttribute('value'));
        var newval = event.target.classList.contains('up') ? p_num+1 : event.target.classList.contains('down') ? 
p_num-1 : event.target.value;
        
        if (parseInt(newval) < 1 || parseInt(newval) > 99) { return false; }


        item.setAttribute('value', newval);
        item.value = newval;


        var price = item.parentElement.parentElement.previousElementSibling.firstElementChild.getAttribute('value');
        item.parentElement.parentElement.nextElementSibling.textContent = (newval * price).formatNumber()+"원";
        //AJAX 업데이트 전송


        //전송 처리 결과가 성공이면    
        this.reCalc();
        this.updateUI();
    },
    //상품 삭제
    delItem: function () {
        event.target.parentElement.parentElement.parentElement.remove();
    }
}

화면 출력하는 메서드중에 formatNumber() 메서드는 숫자 3자리 단위로 콤마를 찍어주는 함수입니다.

아래처럼 프로토타입으로 메서드를 만들어 전역으로 사용합니다.

자주 사용하는 기능이므로 라이브러리로 따로 분리해 공통 메서드로 사용하면 유용합니다.

// 숫자 3자리 콤마찍기
Number.prototype.formatNumber = function(){
    if(this==0) return 0;
    let regex = /(^[+-]?\d+)(\d)/;
    let nstr = (this + '');
    while (regex.test(nstr)) nstr = nstr.replace(regex, '$1' + ',' + '$2');
    return nstr;
};