장바구니의 체크항목만 주문서로 전달

기여·2024년 8월 6일
0

소소한 개발팁

목록 보기
69/103

기획의도:
장바구니에 담은 상품 중 일부만 골라 먼저 주문 시, 해당 상품의 정보 및 결제금액만 출력

쇼핑몰 처음인데 욕심 나름 내기에 cart & order의 애매한 경계가 아직 나에게 새삼스럽다(?). 소스 파일을 아래처럼 나누는 게 맞나 싶지만 더 나아갈 나중을 위해 일단 이렇게 진행해보자.

/
cartid: 각 장바구니 항목의 고유 ID
cartIds: cart에서 선택된 이 cartid들을 담아 order로 전달하는 리스트
/

1, Mapper
1.1, CartMapper

@Select("<script>"
	            + "SELECT cartid, uid, c.product_code, pquantity, "
	            + "pname, pprice, pdelifee "
	            + "FROM cart c JOIN products p ON c.product_code = p.product_code "
	            + "WHERE uid = #{uid} "
	            + "AND cartid IN "
	            + "<foreach item='cartId' collection='cartIds' open='(' separator=',' close=')'>"
	            + "#{cartId}"
	            + "</foreach>"
	            + "</script>")
	    List<CartVo> getCartItemsByIds(@Param("uid") String uid, 
	    		@Param("cartIds") List<String> cartIds);
        // uid와 체크된 cartId 리스트에 해당하는 cart 항목들 조회
		// 해당 사용자가 선택한 장바구니 아이템 목록(CartVo 객체의 리스트)을 반환

1.2, OrdMapper

// 주문서 작성 준비 방법1 - table 2개 조인하고 쿼리 임시로 2개 사용
		//1, 체크된 cart 정보 받아오기
		@Select("<script>"
				+ "SELECT cartid, uid, c.product_code, pquantity, "
				+ "pname, pprice, pdelifee "
				+ "FROM cart c "
				+ "JOIN products p ON c.product_code = p.product_code "
				+ "WHERE uid = #{uid} "
				+ "AND cartid IN "
				+ "<foreach item='cartId' collection='selectedCartItems' open='(' separator=',' close=')'>"
				+ "#{cartId}"
				+ "</foreach>"
				+ "</script>")
		List<OrdVo> prepareOrder(@Param("uid") String uid, 
				@Param("selectedCartItems") List<String> selectedCartItems);
	    
	    //2, 주문서에 표시될 user정보 불러오기
		@Select ("select uname, utel, umail, uadd, detailAddress "
				+ "from users "
				+ "where uid=#{uid}")	
		UserVo userInfo(@Param("uid") String uid);
    
		
// 주문서 작성 준비 방법2 - 추후 table 3개 조인하고 쿼리 하나만 사용

2, Service
2.1, CartSvc

// 해당 사용자가 선택한 장바구니 아이템 목록(CartVo 객체의 리스트)을 반환
		public List<CartVo> getCartItemsByIds(String uid, List<String> cartIds) {
	        return cartMapper.getCartItemsByIds(uid, cartIds);
	    }	

2.2, OrdSvc

// 주문서 작성 준비 위해 체크된 cart 정보 받아오기
	public List<OrdVo> prepareOrder(String uid, List<String> selectedCartItems) {
        return ordMapper.prepareOrder(uid, selectedCartItems);
	}
	
//orderForm에서 user정보 불러오기
	public UserVo userInfo(String uid) {
        return ordMapper.userInfo(uid);
	}

3, CartCtrl (cart만)

@PostMapping("order") //cart.html > action="/cart/order"
	public String order(Model model, @RequestParam List<String> cartIds) {
	    System.out.println("cart에서 order로 이동");

	    String uid = (String) session.getAttribute("username");
	    
	    // 수령지 입력 위해 가입정보 조회
	    model.addAttribute("userinfo", ordSvc.userInfo(uid));	    
	    
	    // 체크된 항목만 가져오기
	    // username과 cartIds를 사용하여 해당 사용자가 선택한 장바구니 아이템 목록(CartVo 객체의 리스트)을 반환
	    List<CartVo> selectedCartItems = cartSvc.getCartItemsByIds(uid, cartIds);
	    System.out.println("selectedCartItems: " + selectedCartItems); //CartVo
	    
	    // 주문서 작성 준비
	    //selectedCartItems를 바탕으로 주문에 필요한 데이터 최종 리스트(OrdVo 객체의 리스트)를 생성하고 처리
	    List<OrdVo> orderItems = ordSvc.prepareOrder(uid, cartIds);
	    
	    System.out.println("prepareOrder cartIds: " + cartIds); // [C00034, ...]
	    
	    // 모델에 선택된 아이템 목록 추가
	    model.addAttribute("orderItems", orderItems);
	    System.out.println("orderItems 추가: " + orderItems); //OrdVo
	    
	    //총계값 전달
	    model.addAttribute("totalPay", calculateTotal(orderItems));
	    model.addAttribute("fmtTotalPay", formatNumber(calculateTotal(orderItems)));
	    
	    //소계값 전달
	    model.addAttribute("hapList", calculateHap(orderItems));
	    model.addAttribute("fmtHapList", orderItems.stream()
	    	    .map(item -> formatNumber(item.getPprice() * item.getPquantity() + item.getPdelifee()))
	    	    .collect(Collectors.toList()));

	    System.out.println("order 끝");
	    
	    return "ord/orderForm"; //ord폴더 내 orderForm.html 양식으로 이동
	}
	
	//총계 계산식
	private int calculateTotal(List<OrdVo> orderItems) {
	    return orderItems.stream().mapToInt(item -> 
	        item.getPprice() * item.getPquantity() + item.getPdelifee()).sum();
	}
	
	//소계 계산식	
	private List<Integer> calculateHap(List<OrdVo> orderItems) {
	    return orderItems.stream()
	        .map(item -> item.getPprice() * item.getPquantity() + item.getPdelifee())
	        .collect(Collectors.toList());
	}
	
	//천단위 콤마 fmt - vo/front보다 ctrl에서 처리할 것 권장됨
	private String formatNumber(int number) {
	    NumberFormat numberFormat = NumberFormat.getNumberInstance(Locale.getDefault());
	    return numberFormat.format(number);
	}

4, html
4.1, cart.html

<h1 th:text="'cart (' + ${lisize} + '건)'"></h1>
<br>

<form action="/cart/order" method="post">

	<input type="hidden" name="uid" th:value="${session.username}">
	
	<div style="width: 800px; display: flex; flex-direction: column; align-items: center;">	 <!-- mediaList -->
		
		<div style="width: 100%; display: flex; justify-content: flex-end;"> <!-- totalPay & 주문버튼-->
		
			<input type="hidden" name="totalPay" id="totalPayInput" th:value="${totalPay}"><!-- totalPay값 입력 받을 영역 -->    
		    <span id="totalPayText"></span><!-- totalPay값, 즉 예상결제액 표시할 영역 -->
		    
		    &emsp;
		    <button type="submit" id="orderButton" class="button">주문하기</button>
			<!-- 	<input type="submit" id="orderButton" value="주문하기" class="button"> 
			css 때문에 input > button으로 변경 -->		    
		    	
		</div>	
		<br>
	
		<div th:each="m : ${li}" class="mediaObject" style="width: 100%; margin: 15px;
		display: flex; flex-wrap: wrap; justify-content: left; background-color: #f4f4f4; 
		border-radius: 5px; box-shadow: 0 3px 5px #96a9fe, 0 2px 4px #96a9fe;"><!-- mediaObject -->
		
			<!-- 			1 -->
				<div>
					<input type="checkbox" name="ckbox" th:value="${m.cartid}" checked>
				</div>
				
				<input type="hidden" name="cartIds" th:value="${m.cartid}">
				<!-- cartIds: /cart/order에 전달할 값 = 선택된 cart들 -->
				
				<div style="margin-left: 10px;">
					<a th:href="@{/prd/viewPrd(product_code=${m.product_code})}" target="_blank">
			            <img th:src="@{/img/}+${m.pimgStr}" width="150" />
			        </a>
				 </div>			
			<!-- 			1 끝 -->			
			
			<!-- 			2 -->
				<div style="text-align: left; margin-left: 10px;">
					<div>
						 <a th:href="@{/prd/viewPrd(product_code=${m.product_code})}" 
						 th:utext="'<strong>' + ${m.pname} + '</strong>'" target="_blank">
						</a>
					</div>
					
					<!-- 	수직 정렬 위해 span 대신 div 씀. span 쓰면서 수직은 유지하려면 각각 div으로 감싸기 -->
					<div>						
			        <input type="hidden" name="pprice" th:value="${m.pprice}">	
			        <span th:text="'판매가: ' + ${m.fmtPprice} + '원'"></span>
		        	</div>			
		        	            			            
			        <div>
			        <input type="hidden" name="pdelifee" th:value="${m.pdelifee}">
			        <span th:text="${m.pdelifee == 0 ? '무료배송❗' : '배송비: ' + m.fmtPdelifee + '원'}"></span>
			        </div>
		            <!-- 	수직 정렬 위해 span 대신 div 씀 -->
				</div>				
			<!-- 			2 끝 -->			
			
			<!-- 			3 -->
				<div style="margin-left: 100px; margin-right: 10px; display: flex; flex-direction: column; text-align: right;">
					<div>
					주문수량: 
						<input type="number" name="pquantity" class="quantity-input" 
						th:value="${m.pquantity}" th:text="'/ ' + ${m.pstock} + '개'" min="1" th:max="${m.pstock}">
						<!-- class="quantity-input" 통해 입력된 주문량을 js에 전달 -->
						
					</div>
				</div>
				
				<div style="margin: 10px 10px auto auto; display: flex; flex-direction: column; text-align: right;">
				    
					<span class="item-total" th:text="'소계: ' + (${m.pprice * m.pquantity + m.pdelifee}) + '원'">소계</span>					

				</div>
				
				<div style="margin-left: auto; margin-right: 10px; display: flex; flex-direction: column; text-align: right;">					
						<a th:href="@{/cart/delCart(cartid=${m.cartid})}" >✖️</a>
				</div>
			<!-- 			3 끝 -->
			
		</div> <!-- mediaObject 끝 -->
	</div> <!-- mediaList 끝 -->

</form>

4.2, orderForm.html

<h1>orderForm</h1>

<form action="/ord/addOrder" method="post" id="orderForm">
	<input type="hidden" name="uid" th:value="${session.username}">

    <div class="orderForm-container">
    <h3>수령인 정보</h3>
    
	    <!-- 상단 -->
	    <div style="width: 100%; display: flex; justify-content: flex-end; margin-bottom: 10px">
	    	<a href="javascript:void(0);" onclick="clearFields()">🎁 수령인정보 직접입력</a>
	        &nbsp; | &nbsp;
	        <a href="javascript:void(0);" onclick="resetForm()">내정보 불러오기</a>
	        &nbsp;
	    </div>
	    <!-- 상단 끝 -->
    
		<!--  정보입력란 -->		
        <div class="form-group">
            <label for="oname"><strong>oname</strong></label>
            <input type="text" id="oname" name="oname" th:value="${userinfo.uname}" required>
        </div>

        <div class="form-group">
            <label for="otel"><strong>otel</strong></label>
            <input type="text" id="otel" name="otel" th:value="${userinfo.utel}" required>
        </div>

        <div class="form-group">
            <label for="omail"><strong>omail</strong></label>
            <input type="text" id="omail" name="omail" th:value="${userinfo.umail}" required>
        </div>

        <div class="form-group">
            <label for="oadd"><strong>oadd</strong></label>
            <input type="text" id="oadd" name="oadd" th:value="${userinfo.uadd}" required>
            &emsp;
            <button type="button" onclick="execDaumPostcode()" class="findAddr">주소 찾기</button>
        </div>
					
						<input type="hidden" id="postcode" name=postcode placeholder="우편번호">
						<input type="hidden" id="extraAddress" name=extraAddress placeholder="참고항목">

        <div class="form-group">
            <label for="odetailAddress"><strong>odtlAddr</strong></label>
            <input type="text" id="odetailAddress" name="odetailAddress" th:value="${userinfo.detailAddress}" required>
        </div>    	
	<!--  정보입력란 끝 -->
	
	<div class="separator-horizontal"></div> <!------------------------- 수평선 -------------------------->
	</div>
	<h3>주문상품</h3>	

		<!-- orderItems list -->
		<div style="width: 650px; margin: 15px; display: flex; justify-content: flex-end; 
		flex-wrap: wrap; flex-direction: row; align-items: center; justify-content: center;">
		
			<!-- each orderItem -->
			<div th:each="oitem, iter : ${orderItems}" style="width: 100%; 
			display: flex; flex-wrap: wrap; justify-content: center;">
			<!--	iter: 소계 출력용 -->
		
			    <input type="hidden" name="cartid" th:value="${oitem.cartid}">				    
		    	<input type="hidden" name="product_code" th:value="${oitem.product_code}">
				    
			    <div style="width: 120px; text-align: left; margin-left: 15px;">
				    <a th:href="@{/prd/viewPrd(product_code=${oitem.product_code})}" 
					 th:utext="'<strong>' + ${oitem.pname} + '</strong>'" target="_blank">
					</a>
			    </div>
				    
			    <div style="width: 100px; text-align: right; margin-right: 15px;">
				    <input type="hidden" name="pprice" th:value="${oitem.pprice}">
				    <span th:text="${oitem.fmtPprice} + '원'"></span>
			    </div>
	
				<div style="width: 80px; text-align: left;">
				    <input type="hidden" name="pquantity" th:value="${oitem.pquantity}">
				    <span th:text="'* ' + ${oitem.pquantity} + '개'"></span>
			    </div>				    
			    
			    <div style="width: 150px; text-align: left;">				    
				    <input type="hidden" name="pdelifee" th:value="${oitem.pdelifee}">
			        <span th:text="${oitem.pdelifee == 0 ? '[무료배송]' : '+ 배송비: ' + oitem.fmtPdelifee + '원'}"></span>
			    </div>
			    
			    <div style="width: 100px; text-align: right;">				    
				    <input type="hidden" name="subtotal" th:value="${hapList[iter.index]}">
			        <span th:utext="'= <strong>' + ${fmtHapList[iter.index]} + '원</strong>'"></span>
			    </div>
				    
			</div> <!-- each orderItems 끝 -->		
		</div> <!-- orderItems list 끝 -->
	
	<div class="orderForm-container">
	<div class="separator-horizontal"></div> <!------------------------- 수평선 -------------------------->
	</div>
	
	<input type="hidden" name="totalPay" th:value="${totalPay}">
	<span th:utext="'총결제액: <strong>' + ${fmtTotalPay} + '원</strong>'"></span>	
	<br>
			
    <button type="submit" class="button">결제하기</button>	
	
</form>

5, js(cart만 변동 있음)

<script>
//소계, 총계 계산
    document.addEventListener('DOMContentLoaded', function() {
        const quantityInputs = document.querySelectorAll('.quantity-input');
        const checkboxes = document.querySelectorAll('input[name="ckbox"]');
        const totalPayInput = document.getElementById('totalPayInput');
        const totalPayText = document.getElementById('totalPayText');
        const orderButton = document.getElementById('orderButton');
        // id 기준으로 인식하여 input, button tag 모두 적용
        
        // 천 단위에 콤마 찍기
        function formatNumber(number) {
            return number.toLocaleString();
        }
        
        // 계산하기
        function updateTotals() {
            let totalPay = 0;
            let checkedCount = 0;
            quantityInputs.forEach(input => {
                const mediaObject = input.closest('.mediaObject');
                const checkbox = mediaObject.querySelector('input[name="ckbox"]');
                
                const price = parseFloat(mediaObject.querySelector('input[name="pprice"]').value);
                const quantity = parseInt(input.value, 10);
                const deliveryFee = parseFloat(mediaObject.querySelector('input[name="pdelifee"]').value);
                
                const itemTotal = (price * quantity) + deliveryFee;                
                
                console.log('price: ', price);
                console.log('quantity: ', quantity);
                console.log('deliveryFee: ', deliveryFee);
                console.log('itemTotal: ', itemTotal);
                console.log('checkbox: ', checkbox);
                
                // 소계 업데이트
                const totalSpan = mediaObject.querySelector('.item-total');
				totalSpan.innerHTML = '소계: <strong>' + formatNumber(itemTotal) + '원</strong>'; //innerHTML
                
				// 체크된 경우만 합산
                if (checkbox.checked) {
                    totalPay += itemTotal;
                    checkedCount++; // 체크된 항목 수 증가
                }                
                
            });
            totalPayInput.value = totalPay;
            totalPayText.innerHTML = '예상결제액: <strong>' + formatNumber(totalPay) + '원</strong>'; //innerHTML
            orderButton.textContent = checkedCount + '건 주문하기'; // 버튼에 체크된 건수 표시 - button tag 쓴 경우 textContent
			// orderButton.value = checkedCount + '건 주문하기'; // input tag 쓴 경우 value            
        }
		
     	// 수량 변경 시 계산 업데이트
        quantityInputs.forEach(input => {
            input.addEventListener('input', updateTotals);
        });
		
    	 // 체크박스 변경 시 계산 업데이트
        checkboxes.forEach(checkbox => {
            checkbox.addEventListener('change', updateTotals);
        });
     	
        updateTotals();
    });
</script>

<script>
//체크된 cart 없을 경우 주문 버튼 비활성화
document.addEventListener('DOMContentLoaded', function() {
    const orderButton = document.getElementById('orderButton');
    const checkboxes = document.querySelectorAll('input[name="ckbox"]');

    function updateOrderButtonState() {
        const anyChecked = Array.from(checkboxes).some(checkbox => checkbox.checked);
        orderButton.disabled = !anyChecked;
    }

    // 체크박스 상태 변경 시 호출
    checkboxes.forEach(checkbox => {
        checkbox.addEventListener('change', updateOrderButtonState);
    });

    // 초기 상태 업데이트
    updateOrderButtonState();
});
</script>

<script>
//체크된 cart만 내부에 전달
document.addEventListener('DOMContentLoaded', function() {
    const form = document.querySelector('form');
    form.addEventListener('submit', function(event) {
    	
        // 제출 전, 체크된 항목들만 cartIds로 추가
        const selectedIds = [];
        document.querySelectorAll('input[name="ckbox"]:checked').forEach(function(checkbox) {
            selectedIds.push(checkbox.value);
        });

        // 모든 hidden cartIds 제거
        document.querySelectorAll('input[name="cartIds"]').forEach(function(input) {
            input.remove();
        });

        // 선택된 항목들만 cartIds로 추가
        selectedIds.forEach(function(id) {
            const hiddenInput = document.createElement('input');
            hiddenInput.type = 'hidden';
            hiddenInput.name = 'cartIds';
            hiddenInput.value = id;
            form.appendChild(hiddenInput);
        });
    });
});
</script>
profile
기기 좋아하는 여자

0개의 댓글