🍄 장바구니 페이지가 하는 일 🍄
장바구니 페이지에서는 AJAX를 활용해 상품 수향 변경 및 삭제 시
페이지 새로 고침없이 즉시 반영되도록 수현하였고,
총 금액이 0원일 경우 구매 버튼을 비활성화하여
잘못된 결제 흐름을 사전에 차단하였다
🍄 script 전체 구조 🍄
1. DOM 요소 캐싱
2. + / - 버튼 수량 변경
3. 집적 입력 수량 변경
4. AJAX 업데이트 함수
5. 상품 삭제 AJAX
6. 총 금액 계산 + 구매 버튼 제어
[ 변수 ]
const buyBtn = $(".cart-buy-btn");
const cartbody = $("#cart-tbody");
const totalPriceElem = $(".cart-summary .total-price");
buyBtn : 구매 버튼 DOM 요소을 갖고 있음 | 구매 버튼의 활성/비활성 상태를 제어
cartbody : 장바구니 상품들이 들어있는 <tbody> | 장바구니 상품 목록을 담는 컨테이너
totalPriceElem : 장바구니 총금액을 표시하는 DOM 요소 | 장바구니 전체 금액을 화면에 출력하는 표시 담당
[ 버튼으로 개수 변경 ]
$(document).on("click", ".btn-minus, .btn-plus", function(){
const tr = $(this).closest("tr");
const cartPk = tr.data("cartpk");
const input = tr.find("input");
let count = parseInt(input.val()) || 1;
if($(this).hasClass("btn-minus")) count -= 1;
else count += 1;
// 개수 변경 함수 호출
updateCartCount(cartPk, count, tr, input);
});
tr : 클릭된 버튼이 속한 상품 한 줄(tr) 찾기 ➡ 어떤 상품의 버튼을 눌렀는지 식별
cartPk : 서버에서 해당 상품을 식별하는 고유 ID, AJAX로 요청 시 전달됨
input : 해당 상품의 수량 읽기, 이 상품의 상태값 역할
count : 현재 수량을 숫자로 변환
[ 장바구니 안의 개수 변경 ]
// AJAX : 개수변경, 총금액
function updateCartCount(cartPk, count, tr, input){
// 최소/최대 범위 제한
if(count < 1) count = 1;
if(count > 99) count = 99;
$.ajax({
url: "${pageContext.request.contextPath}/UpdateCartItemCountServlet",
type: "POST",
data: { cartPk: cartPk, newCount: count },
success: function(res){
if(res === "true"){
input.val(count);
// 항목 합계 업데이트
const price = parseInt(tr.find(".item-price").text().replace(/,/g,"").replace("원",""));
const total = price * count;
tr.find(".item-total p").text(total.toLocaleString("ko-KR") + "원");
// 촘 금액 함수 호출
updateTotalPrice();
} else {
alert("수량 변경 실패");
}
}
});
}
수량 변경은 여러 이벤트에서 발생하기 때문에
공통함수로 분리하여 처리.
서버에서 변경 성공 응답을 받은 경우에만
수량과 금액 UI를 갱신하도록 설계했습니다
이를 통해 서버 상태와 화면 상태의 불일치를 방지
[ 각 매개변수 역할 ]
| cartPk | 서버에서 장바구니 항목을 식별하는 PK |
| count | 변경하려는 수량 |
| tr | 해당 상품의 행(<tr>) |
| input | 수량 입력창 DOM |
[ 수량 범위 제한 ]
if 조건문으로 하는데 여기서 하는 이유
버튼 이벤트, 직접 입력 이벤트로 진입 경로가 2가지이기 때문에
서버로 보내기 전에 프론트에서 한 번 더 정제
[ AJAX ]
비동기 처리를 하는 부분을 보면 페이지를 새로고침하지 않고 UX를 즉시 반영한다
요청 URL
data : 전송 데이터 (서버에 전달되는 값은 cartPk, newCount )
sussess : 서버 응답 처리
성공했을 때만 UI 변경
서버 성공 ➡ 화면 변경
서버 실패 ➡ 화면 유지
input.val(count); : 수량 input값 동기화
다시 세팅하는 이유는 직접 입력했을 수도 있고, 버튼으로 변경했을 수도 있음
price : 상품별 합계 계산, 화면에 표시된 가격에서 ,과 원 제거 후 숫자로 변환
total : 현재 상품의 합계 계산
updateTotalPrice : 전체 총 금액 재계산
"total은 해당 상품 한줄의 합계" ➡ 부분 업데이트
"updateTotalPrice"는 장바구니 전체 상태를 다시 계산 ➡ 전체 상태 재계산
[ 총 금액 계산 ]
// 총 금액 계산
function updateTotalPrice(){
let total = 0;
$("#cart-tbody tr").each(function(){
// 가격이 없는 행(빈 장바구니 메시지 등)은 스킵
if($(this).find(".item-price").length === 0) return;
const priceText = $(this).find(".item-price").text().replace("원","").replace(/,/g,"");
const count = parseInt($(this).find("input").val()) || 1;
total += parseInt(priceText) * count;
});
// 총 금액 계산 후
if (isNaN(total) || total <= 0) {
total = 0;
buyBtn.prop("disabled", true).addClass("disabled");
} else {
buyBtn.prop("disabled", false).removeClass("disabled");
}
totalPriceElem.text(total.toLocaleString("ko-KR") + "원");
}
장바구니 모든 상품 상태를 다시 읽어서
총 금액 + 구매 가능 여부를 한 번에 정리
total : 장바구니 전체 금액 누적 변수
[ 장바구니 모든 행 순회 ]
$("#cart-tbody tr").each(function(){
의미 : 현재 화면에 존재하는 모든 상품 행(tr) 검사
장바구니가 동적으로 변해도 ➡ 현재 상태 기준으로 계산
[ 빈 바구니 행 제외 ]
if($(this).find(".item-price").length === 0) return;
"빈 장바구니" 행 제외
빈 장바구니일때 출력하는 멘트가 있기 때문에
일단 그리고 가격이나 input이 없어서 NaN 이 발생할 수 있음
priceText : 상품 가격 추출을 하는데 화면에 표시된 가격에서 콤마와 원 제거 ➡ 계산은 숫자 타입
count : 상품 수량 추출, 숫자 아닐 경우 기본값 1
total : 전체 금액 누적
[ 총금액 계산 후 버튼 비활성화/활성화 ]
만약 isNaN 이거나 total이 0보다 작거나 같으면 구매 버튼 비활성화
정상 상태일 경우에는 버튼 활성화 (결제 가능 상태)
[ 총 금액 화면 출력 ]
totalPriceElem.text(total.toLocaleString("ko-KR") + "원");
계산된 총 금액을 사용자에게 시작적으로 표시
cart.jsp 전체코드
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<%@ taglib tagdir="/WEB-INF/tags" prefix="ornably"%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>오너블리 - 함꼐 크리스마스를 즐겨요</title>
<link rel="icon" href="images/ORNABLY.jpg">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="" name="keywords">
<meta content="" name="description">
<!-- Google Web Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
rel="stylesheet">
<!-- Icon Font Stylesheet -->
<link rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.15.4/css/all.css" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css"
rel="stylesheet">
<!-- Libraries Stylesheet -->
<link href="lib/animate/animate.min.css" rel="stylesheet">
<link href="lib/owlcarousel/assets/owl.carousel.min.css"
rel="stylesheet">
<!-- Customized Bootstrap Stylesheet -->
<link href="css/bootstrap.min.css" rel="stylesheet">
<!-- Template Stylesheet -->
<link href="css/style.css" rel="stylesheet">
<!-- Template Stylesheet -->
<link rel="stylesheet"
href="${pageContext.request.contextPath}/customResource/style.css">
<style>
/* 기본 버튼 색상 (비활성화 및 활성화 상태 공통) */
.cart-buy-btn {
background-color: #6F4F3A;
border-color: #6F4F3A;
opacity: 1; /* 기본적으로 불투명도 1로 설정 */
cursor: pointer;
color: white;
}
/* 비활성화 상태 */
.cart-buy-btn.disabled {
opacity: 0.5; /* 비활성화 시 투명도 설정 */
cursor: not-allowed; /* 마우스 커서 변경 */
}
/* 활성화 상태 */
.cart-buy-btn:not(.disabled) {
opacity: 1; /* 활성화 시 opacity 1로 설정 */
}
</style>
</head>
<body>
<ornably:header />
<!-- Single Page Header start -->
<div class="container-fluid page-header py-5">
<h1 class="text-center text-white display-6 wow fadeInUp"
data-wow-delay="0.1s">장바구니</h1>
</div>
<!-- Single Page Header End -->
<!-- 장바구니 품목 Start -->
<div class="container-fluid py-5 cart-section">
<div class="container py-5 content item-card rounded">
<div class="row">
<!-- 왼쪽 장바구니 테이블 -->
<div class="col-lg-8 col-md-7 col-sm-12 mb-5">
<div class="table-responsive card cart-table">
<table class="table">
<thead>
<tr>
<th scope="col"><span style="color:black">상품명</span></th>
<th scope="col"><span style="color:black">가격</span></th>
<th scope="col"><span style="color:black">수량</span></th>
<th scope="col"><span style="color:black">합계</span></th>
<th scope="col"><span style="color:black">삭제</span></th>
</tr>
</thead>
<tbody id="cart-tbody">
<!-- 상품 출력 -->
<c:if test="${empty cartDatas }">
<tr>
<td colspan="5" class="text-center py-5 text-muted"><i
class="bi bi-cart-fill" style="font-size: 40px;"></i>
<p class="mt-3 mb-0">장바구니가 비어있습니다..</p></td>
</tr>
</c:if>
<c:forEach var="cart" items="${cartDatas}">
<tr data-cartpk="${cart.cartPk}">
<th scope="row">
<p class="mb-0 py-4">${cart.itemName}</p>
</th>
<!-- 가격 -->
<td class="item-price">
<p class="mb-0 py-4">
<fmt:formatNumber value="${cart.itemPrice}" type="number" />
원
</p>
</td>
<!-- 수량 -->
<td>
<div class="input-group py-4 cart-quantity"
style="width: 100px;">
<button type="button"
class="btn btn-sm btn-minus rounded-circle bg-light border">-</button>
<input type="text"
class="form-control form-control-sm text-center border-0 item-count"
value="${cart.count != 0 ? cart.count : 1}"
data-cartpk="${cart.cartPk}">
<button type="button"
class="btn btn-sm btn-plus rounded-circle bg-light border">+</button>
</div>
</td>
<!-- 합계 -->
<td class="item-total">
<p class="mb-0 py-4">
<fmt:formatNumber
value="${cart.itemPrice * (cart.count != 0 ? cart.count : 1)}"
type="number" />
원
</p>
</td>
<!-- 삭제 -->
<td class="py-4">
<button
class="btn btn-md rounded-circle bg-light border cart-remove-btn">
<i class="fa fa-times text-danger"></i>
</button>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</div>
<!-- 오른쪽 결제요약 박스 -->
<div class="col-lg-4 col-md-5 col-sm-12">
<div class="rounded cart-summary content item-card">
<div class="p-4">
<h2 class="display-6 mb-4">
장바구니 <span class="fw-normal">총액</span>
</h2>
</div>
<div
class="py-4 mb-4 border-top border-bottom d-flex justify-content-between">
<h5 class="mb-0 ps-4 me-4">총 합계</h5>
<p class="mb-0 pe-4 total-price">${totalPrice}원</p>
</div>
<form action="${pageContext.request.contextPath}/paymentPage.do"
method="get">
<button
class="btn rounded-pill px-4 py-3 text-uppercase mb-4 ms-4 cart-buy-btn"
style="background-color: #6F4F3A; border-color: #6F4F3A;"
type="submit">구매</button>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- 장바구니 품목 End -->
<!-- footer 태그 호출 -->
<ornably:footer />
<!-- jQuery -->
<script>
console.log('${cartDatas}');
const buyBtn = $(".cart-buy-btn");
const cartbody = $("#cart-tbody");
const totalPriceElem = $(".cart-summary .total-price"); // 총액 변수
// 버튼으로 개수 변경
$(document).on("click", ".btn-minus, .btn-plus", function(){
const tr = $(this).closest("tr");
const cartPk = tr.data("cartpk");
const input = tr.find("input");
let count = parseInt(input.val()) || 1;
if($(this).hasClass("btn-minus")) count -= 1;
else count += 1;
// 개수 변경 함수 호출
updateCartCount(cartPk, count, tr, input);
});
// 직접 입력으로 개수 변경
$(document).on("blur", ".item-count", function(){
const input = $(this);
const tr = input.closest("tr");
const cartPk = input.data("cartpk");
let count = parseInt(input.val()) || 1;
if(count < 1) count = 1; // 최소 1
// 개수 변경 함수 호출
updateCartCount(cartPk, count, tr, input);
});
// AJAX : 개수변경, 총금액
function updateCartCount(cartPk, count, tr, input){
// 최소/최대 범위 제한
if(count < 1) count = 1;
if(count > 99) count = 99;
$.ajax({
url: "${pageContext.request.contextPath}/UpdateCartItemCountServlet",
type: "POST",
data: { cartPk: cartPk, newCount: count },
success: function(res){
if(res === "true"){
input.val(count);
// 항목 합계 업데이트
const price = parseInt(tr.find(".item-price").text().replace(/,/g,"").replace("원",""));
const total = price * count;
tr.find(".item-total p").text(total.toLocaleString("ko-KR") + "원");
// 촘 금액 함수 호출
updateTotalPrice();
} else {
alert("수량 변경 실패");
}
}
});
}
// 삭제 버튼
$(document).on("click", ".cart-remove-btn", function(){
const tr = $(this).closest("tr");
const cartPk = tr.data("cartpk");
$.ajax({
url: "${pageContext.request.contextPath}/DeleteCartServlet",
type: "POST",
data: { cartPk: cartPk },
success: function(res){
if(res === "true"){
tr.remove(); // 선택한 상품 삭제
if ($("#cart-tbody tr").length === 0) {
$("#cart-tbody").append(`
<tr>
<td colspan="5" class="text-center py-5 text-muted">
<i class="bi bi-cart-fill" style="font-size: 40px;"></i>
<p class="mt-3 mb-0">장바구니가 비어있습니다..</p>
</td>
</tr>
`);
}
updateTotalPrice(); // 총 금액 업데이트
} else {
alert("삭제 실패");
}
}
});
});
// 총 금액 계산
function updateTotalPrice(){
let total = 0;
$("#cart-tbody tr").each(function(){
// 가격이 없는 행(빈 장바구니 메시지 등)은 스킵
if($(this).find(".item-price").length === 0) return;
const priceText = $(this).find(".item-price").text().replace("원","").replace(/,/g,"");
const count = parseInt($(this).find("input").val()) || 1;
total += parseInt(priceText) * count;
});
// 총 금액 계산 후
if (isNaN(total) || total <= 0) {
total = 0;
buyBtn.prop("disabled", true).addClass("disabled");
} else {
buyBtn.prop("disabled", false).removeClass("disabled");
}
totalPriceElem.text(total.toLocaleString("ko-KR") + "원");
}
$(document).ready(function(){
// 페이지 로딩 시 총 금액 계산
updateTotalPrice();
});
</script>
</body>
</html>
UpdateCartItemCountServlet.java
package controller.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import model.dao.CartDAO;
import model.dto.CartDTO;
@WebServlet("/UpdateCartItemCountServlet")
public class UpdateCartItemCountServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public UpdateCartItemCountServlet() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("[로그] controller.servlet.UpdateCartItemCountServlet | [doGet] - 잘못된 요청 방식 **POST방식 요청**");
doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("[로그] controller.servlet.UpdateCartItemCountServlet | [doPost] - 시작");
String stringCartPk = request.getParameter("cartPk");
String stringNewCount = request.getParameter("newCount");
System.out.println("[로그] controller.servlet.UpdateCartItemCountServlet | [데이터 받기] - cartPk:["+stringCartPk+"] newCount:["+stringNewCount+"]");
int cartPk = Integer.parseInt(stringCartPk);
int newCount = Integer.parseInt(stringNewCount);
CartDAO cartDAO = new CartDAO();
CartDTO cartDTO = new CartDTO();
cartDTO.setCartPk(cartPk);
cartDTO.setNewCount(newCount);
cartDTO.setCondition("UPDATE_CART_ITEM_COUNT");
System.out.println("[로그] controller.servlet.UpdateCartItemCountServlet | [cartDAO.update(cartDTO)] - cartDTO:["+cartDTO+"]");
if(cartDAO.update(cartDTO)) {
System.out.println("[로그] controller.servlet.UpdateCartItemCountServlet | [장바구니 아이템 개수 변경] - 성공");
response.getWriter().print(true);
}
else {
response.getWriter().print(false);
System.out.println("[로그] controller.servlet.UpdateCartItemCountServlet | [장바구니 아이템 개수 변경] - 실패");
}
}
}
'🎅 오너먼트 프로젝트' 카테고리의 다른 글
| jQuery의 이벤트 위임 방식 $(document).on()과 AJAX (1) | 2026.01.05 |
|---|---|
| 장바구니 개수 변경 / 장바구니 상품 삭제 전체 코드 (0) | 2026.01.05 |
| 실제 서비스에서 카카오페이 결제를 안전하게 처리할 때 서버가 필요한 흐름 (0) | 2025.12.29 |
| 로그아웃 "모달창" 띄우기 (0) | 2025.12.28 |
| 결제 API (with 페이 디벨롭퍼) (0) | 2025.12.28 |