쇼핑몰 웹사이트 만들어보기 - 쿠폰 중복 선택 버그 수정

Shiba·2024년 8월 24일
0

프로젝트 및 일기

목록 보기
23/29
post-thumbnail

++2024/08/23 21:30)
현재는 한 html에서 해당 작업들을 모두 수행중

cart 데이터베이스에 쿠폰과 최종가격을 저장하여 최종가격이 존재한다면 해당 셀이 보이도록 하게하고, 팝업은 리뷰창과 같이 새로운 창으로 구성하여 쿠폰함을 받아올 수 있도록 하면 될 것 같다.

++2024/08/24 02:12)
위의 방법대로 장바구니 데이터베이스에 쿠폰을 저장하는 순간, 나갔다 들어와도 해당 쿠폰이 계속 적용된 상태로 남아있게된다. 이 방법으로는 안된다...그래서 롤백을 했다 (갓허브) 다시 처음으로 돌아왔다...

++2024/08/24 07:22)
어제 자려고 누워서 계속 생각해본 결과 장바구니에 쿠폰을 저장하지 않고, 그냥 팝업창만 하나의 html로 분리해서 만들 수 있다면 'window.opener.함수' 를 통해서 내가 원하는 동작이 만들어질 수 있을 것 같다고 생각했다. 이대로 만들어보자! 제 코드를 만지다 알게된 오류인데 결제정보에서 쿠폰정보를 받아서 사용한 쿠폰도 제거되도록 같이 고쳐보자. 그리고 결제 성공 창도 이참에 한번에 꾸며주자

++2024/08/24 17:07)
글을 쓴다는 것은 성공을 했다는 것을 의미한다. 위 방법대로 시도해서 코드를 최대한 고치지 않는 쪽으로 했더니 정상작동함을 확인했다. 코드를 고치지 않으려했기 때문에 코드가 조금 지저분할 수도 있다..(프론트가 어려워요)

뭐 일단 작성한 코드를 정리해보도록하자!

먼저 고친 코드는 결제정보코드인데, 이전에는 쿠폰을 포함하지않아서 쿠폰을 사용해도 제거되지가 않았다. 이를 제거하기위해 쿠폰id를 외래키로 받아서 결제정보에 쿠폰에 대한 정보를 저장해두고 쿠폰 리스트에 저장해둔 해당 쿠폰은 삭제가 되도록 했다.

그러기위해서 쿠폰리스트를 제거하는 코드가 필요해졌기 때문에 리포지토리와 서비스 수정을 하고, 컨트롤러에서 해당 코드를 활용하여 원하는 동작을 하도록 고쳤다.

쿠폰 리포지토리, 서비스에 쿠폰 리스트 삭제기능 추가

쿠폰 리포지토리

public CouponList findFirstCouponList(String user_id, int coupon_id){
        TypedQuery<CouponList> query = em.createQuery("SELECT u FROM CouponList u WHERE u.user.id = :user_id AND u.coupon.id = :coupon_id", CouponList.class);
        query.setParameter("user_id", user_id);
        query.setParameter("coupon_id", coupon_id);
        return query.getResultList().get(0);
    }

    public boolean deleteCouponList(CouponList couponList) {
        if (couponList != null) {
            em.remove(couponList); // 엔티티 삭제
            return true;
        }
        return false;
    }

쿠폰 서비스

public boolean deleteFirstCouponList(String user_id, int Coupon_id) {
        CouponList couponList = couponRepository.findFirstCouponList(user_id, Coupon_id);
        return couponRepository.deleteCouponList(couponList);
    }

혹시나 나중에 같은 쿠폰을 중복해서 발급받을 수 있도록 할 수도 있기에 위 코드처럼 가장 먼저 발견하는 쿠폰리스트를 추출해서 해당 쿠폰리스트를 제거하는 방식을 취하도록 했다

결제 컨트롤러 수정

//코드 생략

// 결제 성공 및 실패 비즈니스 로직을 구현하세요.
        int len = cartIds.length;
        String userId = userDetails.getUsername();
        for(int i = 0; i<len; i++){
            Purchases purchases = new Purchases();
            purchases.setUser_id(userId);
            purchases.setUsers(userService.findById(userId));
            Products p = cartService.findById(cartIds[i]).getProducts();
            purchases.setProduct_id(p.getId());
            purchases.setPurchase_type(paymentType);
            purchases.setProducts(p);
            purchases.setCoupon(couponService.findCouponById(couponIds[i]));
            purchases.setProduct_cnt(quantity[i]);
            purchases.setPrice(price[i]);
            purchases.setOrder_id(orderId);
            purchaseService.addPurchase(purchases);
           //쿠폰 리스트 제거 코드 추가
           	couponService.deleteFirstCouponList(userId,couponIds[i]); 
            cartService.deleteCartItem(cartIds[i]);
        }

프론트는 파라미터를 받아오는 방식을 그냥 복붙해서 만들었기에 생략하도록 하겠다. 쿠폰id를 이전에 받아오던 퍼센트대신 받아오면 되는 것이다


쿠폰 중복 선택 가능 버그 수정

이제 오늘의 주제인 쿠폰 중복 선택 가능 버그를 해결한 코드를 살펴보자

코드의 대부분은 프론트에서 해결이 가능하지만 백엔드에서도 당연히 수정이 필요하다. 백엔드에서 쿠폰함 팝업에 보낼 모델을 위해서이다.

백엔드 코드 수정(컨트롤러 수정)

쿠폰 컨트롤러 - 쿠폰 팝업 GET매핑

@GetMapping("/couponPopup")
    public String getCoupons(@AuthenticationPrincipal UserDetails userDetails,
                             @RequestParam(name = "price") int price,
                             @RequestParam(name = "useCouponId") int useCouponId,
                             @RequestParam(name = "cartItemId") int cartItemId,
                             @RequestParam(name = "usedCouponIds", required = false) List<Integer> usedCouponIds,
                             Model model) {
        List<Coupon> coupons = couponService.makeRealCouponList(couponService.findListByUserId(userDetails.getUsername()));
        List<Coupon> excludeCoupon = new ArrayList<>();
        Coupon useCoupon = null;
        if(usedCouponIds!=null && coupons != null){
            for (Coupon c: coupons) {
                boolean canUse = true;
                for (int id: usedCouponIds) {
                    if(c.getId() == useCouponId){
                        useCoupon = c;
                        break;
                    }

                    if(c.getId() == id) {
                        canUse = false;
                        break;
                    }
                }
                if(canUse) excludeCoupon.add(c);
            }
        }
        if(useCoupon == null){
            useCoupon = new Coupon();
            useCoupon.setId(0);
        }
        model.addAttribute("coupons", excludeCoupon);
        model.addAttribute("price", price);
        model.addAttribute("useCoupon", useCoupon);
        model.addAttribute("cartItemId", cartItemId);
        return "/cart/couponPopup"; 
    }

쿠폰 팝업에서는 쿠폰리스트를 받아 쿠폰을 표시해줄건데, 중복을 막기 위해서 사용한 쿠폰들은 리스트에 추가시키지 않을 것이다. 다만, 변경 시에는 이미 적용되어있는 쿠폰을 표시해주도록하기 위해 useCouponId를 받아 useCoupon을 모델에 추가하여 사용하도록 하겠다.

프론트엔드 코드 수정

여기가 아마 가장 분량이 많을 것이다. 기존의 존재하던 display:none을 활용한 팝업을 완전히 지우고 리뷰 팝업과 같이 새로운 창으로 쿠폰함을 열 것이기 때문이다.

cart.html 수정

                <span class="discounted-price" data-discount="0" data-coupon-id="0" style="margin-top: 23px; margin-right: 10px;"></span>

장바구니 html자체에는 수정할 것이 data-coupon-id정도인데, 이를 추가하여 이미 적용되어있는 쿠폰이 있다면 팝업에서 표시해주는데에 사용할 것이다. (당연히 팝업관련 코드는 모두 지웠다)

cart.js 수정

$(document).ready(function() {
  //...생략...//
  
 // 각 쿠폰 적용 버튼에 이벤트 리스너 추가
    document.querySelectorAll('.coupon-button').forEach(button => {
        button.addEventListener('click', (event) => {
            const row = event.target.closest('tr'); // 버튼이 포함된 행 찾기
            var cartItemId = row.getAttribute('data-cart-item-id');
            let quantity = parseInt(row.querySelector('.quantity').textContent, 10);
            let usedCouponIds = [];
            document.querySelectorAll('.discounted-price').forEach(function (discountedPriceSpan) {
                const couponId = discountedPriceSpan.getAttribute('data-coupon-id');
                if (couponId) {
                    usedCouponIds.push(couponId);
                }
            });

            const useCouponId = row.querySelector('.discounted-price').getAttribute('data-coupon-id') || 0;
            let productPrice = parseFloat(row.querySelector('.origin-price').textContent);
            let totalPrice = productPrice * quantity;

            const url = `/couponPopup?price=${encodeURIComponent(totalPrice)}&useCouponId=${encodeURIComponent(useCouponId)}&cartItemId=${encodeURIComponent(cartItemId)}&usedCouponIds=${encodeURIComponent(usedCouponIds.join(','))}`;
            const windowFeatures = 'width=300,height=600,menubar=no,toolbar=no,location=yes,status=no,resizable=no,scrollbars=yes';

            window.open(url, 'CouponPopup', windowFeatures);


            const changeButton = row.querySelector('.change-button');
            const cancelButton = row.querySelector('.cancel-button');

            // 변경 버튼 클릭 시 처리 (예: 다시 쿠폰 적용)
            changeButton.addEventListener('click', (event) => {
                var cartItemId = row.getAttribute('data-cart-item-id');
                let quantity = parseInt(row.querySelector('.quantity').textContent, 10);
                let usedCouponIds = [];
                document.querySelectorAll('.discounted-price').forEach(function (discountedPriceSpan) {
                    const couponId = discountedPriceSpan.getAttribute('data-coupon-id');
                    if (couponId) {
                        usedCouponIds.push(couponId);
                    }
                });

                const useCouponId = row.querySelector('.discounted-price').getAttribute('data-coupon-id') || 0;
                let productPrice = parseFloat(row.querySelector('.origin-price').textContent);
                let totalPrice = productPrice * quantity;

                const url = `/couponPopup?price=${encodeURIComponent(totalPrice)}&useCouponId=${encodeURIComponent(useCouponId)}&cartItemId=${encodeURIComponent(cartItemId)}&usedCouponIds=${encodeURIComponent(usedCouponIds.join(','))}`;
                const windowFeatures = 'width=300,height=600,menubar=no,toolbar=no,location=yes,status=no,resizable=no,scrollbars=yes';
                window.open(url, 'CouponPopup', windowFeatures);
            });

            cancelButton.addEventListener('click', () => {
                const discountedPriceCell = row.querySelector('.discounted-price-cell');
                const discountPriceSpan = discountedPriceCell.firstElementChild.firstElementChild;
                discountPriceSpan.setAttribute('data-discount', "0"); // 할인율 저장
                discountPriceSpan.setAttribute('data-coupon-id', "0"); // 할인율 저장
                discountedPriceCell.style.display = 'none';

                const applyCell = row.querySelector('.coupon-apply-cell');
                applyCell.style.display = 'table-cell';

                updatePriceAndTotal();
            });

        });
    });
  });

function updatePriceAfterCoupon(cartItemId, couponId,finalPrice, percent) {
    // 할인된 가격을 테이블에 표시
    // cartItemId를 사용해 tr 요소 찾기
    const row = document.querySelector(`tr[data-cart-item-id='${cartItemId}']`);
    const discountedPriceCell = row.querySelector(`.discounted-price-cell`);
    const discountPriceSpan = discountedPriceCell.firstElementChild.firstElementChild;
    discountPriceSpan.textContent = `${finalPrice.toFixed(0)}`;
    discountPriceSpan.setAttribute('data-discount', percent.toString()); // 할인율 저장
    discountPriceSpan.setAttribute('data-coupon-id', couponId.toString()); // 쿠폰Id 저장
    discountedPriceCell.style.display = 'table-cell';
    discountPriceSpan.nextElementSibling.style.display='grid';
    const applyCell = row.querySelector('.coupon-apply-cell');
    applyCell.style.display = 'none';

    // 총 가격도 업데이트가 필요할 수 있음
    updatePriceAndTotal();
}

function updatePriceAndTotal() {
    var totalAmount = 0;
    $('tbody tr').each(function() {
        const discountPercent = parseInt($(this).find('.discounted-price').attr('data-discount'), 10) || 0;
        var price = 0;
        if(discountPercent > 0) price = parseInt($(this).find('.discounted-price').text().replace('원', ''));
        else price = parseInt($(this).find('td:nth-child(3)').text().replace('원', ''));

        //console.log($(this).find('.discounted-price').attr('data-discount'));
        totalAmount += price;
    });
    $('#totalAmount').text(totalAmount + '원');
}

여기가 아마 이번 글에서 가장 많은 변경이 있는 곳일 것이다. 일단 팝업과 관련된 코드를 모두 삭제했다. 그리고, 위의 코드들을 추가했다. 쿠폰 적용 코드에서는 리뷰 팝업에서 했듯이 요소를 파라미터로 보내어 팝업에서 사용할 수 있도록 할 것이다.

이 코드에서 가장 중요한 코드라면 아마 updatePriceAfterCoupon 함수일 것이다. 이 코드를 팝업에서 적용하기 버튼을 누르면 실행이 되도록 만들었기 때문에 모든 동작이 가능하게 되는 것이다.

쿠폰 팝업 코드 작성하기

이 코드는 리뷰 팝업과 챗지피티를 참고했다. (챗지피티가 오류는 못잡아도 기본적인 코드의 틀은 잘 짜주어 많이 애용한다)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>쿠폰함</title>
    <link rel="stylesheet" href="/css/couponPopup.css">
    <script>
        document.addEventListener("DOMContentLoaded", function () {
            let useCouponId = [[${useCoupon.id}]];
            let price = originPrice;
            if(useCouponId > 0){
                let percent = [[${useCoupon.percent}]];
                let discount = (price * (1 - percent/100)).toFixed(0);
                price = discount;
            }
            document.getElementById("price").innerText = price + " 원";
        })

        let originPrice = [[${price}]];
        let cartItemId = [[${cartItemId}]];

        function applyCoupon() {
            const selectedCouponId = document.querySelector('input[name="couponId"]:checked').value;
            const percent = parseInt(document.querySelector('input[name="couponId"]:checked').dataset.percent ,10);
            const price = originPrice * (1 - percent/100);
            console.log(percent);
            console.log(price);
            window.opener.updatePriceAfterCoupon(cartItemId, selectedCouponId,price,percent);
            // 팝업 창 닫기
            window.close();
        }

        document.addEventListener('change', (e) => {
            if (e.target.classList.contains('coupon-option')) {
                const discount = parseInt(e.target.dataset.percent, 10);
                const totalPrice = originPrice;
                const discountedPrice = totalPrice * (1 - discount / 100);
                document.getElementById("price").textContent = `${discountedPrice.toFixed(0)}`;
            }
        });

    </script>
</head>
<body>
<h3>쿠폰함</h3>
<div id="container">
    <div id="couponList" th:each="coupon : ${coupons}">
        <input class="coupon-option" type="radio" name="couponId" th:checked="${useCoupon.id} == ${coupon.id}" th:data-percent="${coupon.percent}" th:value="${coupon.id}" th:id="'coupon-' + ${coupon.id}"/>
        <label name="percent" th:for="'coupon-' + ${coupon.id}" th:text="${coupon.percent}+'% 할인쿠폰'"></label>
    </div>

    <input type="hidden" name="price" th:value="${price}"/>
</div>
<div id="footer">
    <div id="discounted-price">
        할인된 가격: <span id="price" class="discounted-price-value"></span>
    </div>
    <!-- 선택한 쿠폰 적용 버튼 -->
    <button type="button" onclick="applyCoupon()">적용하기</button>
</div>
</body>
</html>

th:checked는 다음 조건이 맞으면 체크상태로 두겠다는 것인데, 적용중인 쿠폰이 있었다면 이를 쿠폰팝업이 열릴 때부터 이미 체크가 되어있는 상태로 뜨도록 했다. 이전 팝업과 마찬가지로 체크상태 변경시 할인된 가격이 변경되며, 쿠폰 적용시 'window.opener.updatePriceAfterCoupon(cartItemId,selectedCouponId,price,percent)'를 동작하여 앞서 말했듯이 부모창에서 해당 함수가 실행되어 가격을 업데이트함과 동시에 테이블을 변경시킨다.


코드 리뷰는 끝났다. 이제 테스트 결과를 보도록 하자.

실험용 쿠폰 데이터베이스에 다음 정보들을 추가하고

위 쿠폰들을 발급받은 상태에서 테스트를 진행했다.

쿠폰 중복 사용 가능 버그 수정 테스트

결제까지 흐름.gif

여기서 75%와 85% 쿠폰을 사용해서 결제를 진행했다.
또한 결제 성공창도 대충? 꾸며서 사용자가 사용가능하도록 했다.
주문 내역 보러가기를 누르면 주문내역으로 이동된다(gif가 짤렸다...)

주문내역으로 이동해보면


다음과 같이 결제가 성공적으로 되었음을 확인할 수 있고,


쿠폰도 제거가 되었음을 확인했다!

++) 역시 좀 아쉬워서 다시 gif를 땃다

(이때까지 만든 기능들이 모두 연결되어 이렇게 나오는게 참 짜릿하다..)


오늘의 교훈 : 삽질이 시간면에서는 손해였을지 모르나 그덕에 새로운 지식을 알게되고 그 지식을 통해 새로운 방법을 찾아낼 수도 있다는 점에서 많이 해볼만한 것 같다.
그리고 프론트가 아무리봐도 코드 작성 난이도가 더 어려운 것같다...
(내가 이해도가 부족한 걸수도?)

다음 글에서는 나머지 오류들을 수정해보고 슬슬 진짜 프로젝트를 끝내보자

profile
모르는 것 정리하기

0개의 댓글