쇼핑몰 웹사이트 만들어보기 - 쿠폰 기능

Shiba·2024년 8월 9일
0

프로젝트 및 일기

목록 보기
16/29

오늘은 쿠폰 기능을 적용해보았다. 아직 자잘한 버그가 존재하지만 어느정도 윤곽은 잡힌 것 같다.

먼저, 쿠폰 데이터베이스 테이블을 만들고, 쿠폰의 id와 발급받은 사용자의 id를 이용해 쿠폰리스트 라는 데이터베이스 테이블을 만들었다.

쿠폰은 하나이지만, 이를 발급받을 수 있는 유저는 여러명이기 때문에 이를 리스트라는 테이블을 이용하여 저장할 수 있도록 한 것이다.

쿠폰 엔티티

@Entity
public class Coupon {

    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(name = "valid_term")
    private int validTerm;

    @Column(name = "percent")
    private int percent;

쿠폰 리스트 엔티티

@Entity
public class CouponList {

    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String own_id;

    @ManyToOne
    @JoinColumn(name = "own_id", insertable = false, updatable = false)
    private Users user;

    private int coupon_id;

    @ManyToOne
    @JoinColumn(name = "coupon_id", insertable = false, updatable = false)
    private Coupon coupon;

그러고나서는 늘 그렇듯 리포지토리 서비스 컨트롤러를 만들고, 프론트 코드를 만들면 사용할 수 있을 것이다.

CouponRepository

package com.shoppingmall.repository;

import com.shoppingmall.domain.Coupon;
import com.shoppingmall.domain.CouponList;
import com.shoppingmall.domain.Users;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class CouponRepository {

    @PersistenceContext
    private final EntityManager em;

    public CouponRepository(EntityManager em) {
        this.em = em;
    }

    //쿠폰 적용 메소드
    public int useCoupon(int price, Coupon coupon){
        return price*(1-(coupon.getPercent()/100));
    }

    //쿠폰 유효기간 표시 메소드
    public LocalDate createdDate(Coupon coupon){
        LocalDate currentDate = LocalDate.now();
        return currentDate.minusDays(coupon.getValidTerm());
    }

    public LocalDate expiredDate(Coupon coupon){
        LocalDate currentDate = LocalDate.now();
        return currentDate.plusDays(coupon.getValidTerm());
    }

    //소유자id로 쿠폰 리스트 찾기
    public List<CouponList> findListByUserId(String id){
        TypedQuery<CouponList> query = em.createQuery("SELECT u FROM CouponList u WHERE u.user.id = :id", CouponList.class);
        query.setParameter("id", id);
        return query.getResultList();
    }

    //쿠폰id로 쿠폰 리스트 찾기
    public List<CouponList> findListByCouponId(int id){
        TypedQuery<CouponList> query = em.createQuery("SELECT u FROM CouponList u WHERE u.coupon.id = :id", CouponList.class);
        query.setParameter("id", id);
        return query.getResultList();
    }

    public Coupon findCouponById(int id){
        return em.find(Coupon.class, id);
    }

    public List<Coupon> makeRealCouponList(List<CouponList> couponList){
        List<Coupon> coupons = new ArrayList<>();
        for (CouponList c: couponList) {
            coupons.add(c.getCoupon());
        }
        return coupons;
    }

    public boolean duplicateCoupon(List<CouponList> couponLists, Coupon coupon){
        for (CouponList c: couponLists) {
            if(c.getCoupon_id() == coupon.getId())
                return true;
        }
        return false;
    }

    public CouponList saveCoupon(Users user, Coupon coupon){
        List<CouponList> list = findListByUserId(user.getId());
        if(duplicateCoupon(list,coupon)) return null;

        CouponList couponList = new CouponList();
        couponList.setUser(user);
        couponList.setCoupon(coupon);
        couponList.setOwn_id(user.getId());
        couponList.setCoupon_id(coupon.getId());
        em.persist(couponList);
        return couponList;
    }

    public List<Coupon> getAllCoupon(){
        String query = "SELECT c FROM Coupon c";
        return em.createQuery(query, Coupon.class).getResultList();
    }

}

CouponService

package com.shoppingmall.service;

import com.shoppingmall.domain.Coupon;
import com.shoppingmall.domain.CouponList;
import com.shoppingmall.domain.Users;
import com.shoppingmall.repository.CouponRepository;
import jakarta.transaction.Transactional;

import java.time.LocalDate;
import java.util.List;

@Transactional
public class CouponService {

    private final CouponRepository couponRepository;

    public CouponService(CouponRepository couponRepository) {
        this.couponRepository = couponRepository;
    }

    public int useCoupon(int price, Coupon coupon) { return couponRepository.useCoupon(price, coupon);}

    public LocalDate createdDate(Coupon coupon){
        return couponRepository.createdDate(coupon);
    }


    public LocalDate expiredDate(Coupon coupon){
        return couponRepository.expiredDate(coupon);
    }

    public List<CouponList> findListByUserId(String id){
        return couponRepository.findListByUserId(id);
    }

    public List<CouponList> findListByCouponId(int id){
        return couponRepository.findListByCouponId(id);
    }

    public List<Coupon> makeRealCouponList(List<CouponList> couponList){
        return couponRepository.makeRealCouponList(couponList);
    }

    public Coupon findCouponById(int id){
        return couponRepository.findCouponById(id);
    }

    public boolean saveCoupon(Users user, Coupon coupon){
        CouponList couponList = couponRepository.saveCoupon(user,coupon);
        if(couponList != null) return true;
        return false;
    }

    public List<Coupon> getAllCoupon(){
        return couponRepository.getAllCoupon();
    }
}

CouponController

package com.shoppingmall.controller;

import com.shoppingmall.domain.Coupon;
import com.shoppingmall.domain.Users;
import com.shoppingmall.service.CouponService;
import com.shoppingmall.service.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@Controller
public class CouponController {

    private final CouponService couponService;
    private final UserService userService;

    public CouponController(CouponService couponService, UserService userService) {
        this.couponService = couponService;
        this.userService = userService;
    }

    @PostMapping("/saveCoupon")
    public ResponseEntity<String> useCoupon(@RequestParam(name = "coupon_id") int couponId,
                                            @AuthenticationPrincipal UserDetails userDetails) {
        ResponseEntity response = null;
        try {
            Coupon coupon = couponService.findCouponById(couponId);
            Users user = userService.findById(userDetails.getUsername());
            if(couponService.saveCoupon(user,coupon)){
                response = ResponseEntity
                        .status(HttpStatus.CREATED)
                        .body("쿠폰이 발급되었습니다!");
            }
            else{
                response = ResponseEntity
                        .status(HttpStatus.CREATED)
                        .body("이미 발급된 쿠폰입니다.");
            }
        } catch (Exception ex) {
            response = ResponseEntity
                    .status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("An exception occured due to " + ex.getMessage());
        }
        return response;
    }

    @GetMapping("/getCoupon")
    public ResponseEntity<List<Coupon>> getCoupons(@AuthenticationPrincipal UserDetails userDetails) {
        List<Coupon> coupons = couponService.makeRealCouponList(couponService.findListByUserId(userDetails.getUsername()));
        return ResponseEntity.ok(coupons); // 200 OK 상태 코드와 함께 쿠폰 리스트 반환
    }

    @GetMapping("/event")
    public String eventCouponPage(Model model){
        List<Coupon> coupons = couponService.getAllCoupon();
        model.addAttribute("coupons", coupons);
        return "/event";
    }



}

쿠폰은 장바구니 창의 쿠폰 적용창에서 사용할 것이므로, 장바구니 페이지를 수정하였다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>장바구니</title>
    <link rel="stylesheet" href="/css/cart.css">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="/js/cart.js"></script>
</head>
<body>
<div class="header">
    <a href="/" id="home_logo">
        <img src="/images/icons/logo.png" alt="Home Logo"/>
    </a>
</div>
<h2>장바구니</h2>
<table>
    <thead>
    <th>장바구니 목록</th>
    </thead>
    <tbody>
    <tr class="clickable-row" th:each="cart, iterStat : ${carts}" th:data-cart-item-id="${cart.id}">
        <td id="product"><img th:src="@{'/images/uploads/' + ${cart.products.photo}}" alt="Product Image" height="80" width="80">
            <span id="name" th:text="${cart.products.product_name}"></span></td>
        <td>
            <div class="quantity-controls">
                <button type="button" class="decrease-quantity" th:data-cart-item-id="${cart.id}">-</button>
                <span class="quantity" th:text="${cart.quantity}"></span>
                <span class="origin-price" style="visibility: hidden" th:text="${cart.products.price}"></span>
                <button type="button" class="increase-quantity" th:data-cart-item-id="${cart.id}">+</button>
            </div>
        </td>
        <td class="total-price" th:text="${cart.products.price * cart.quantity} + '원'"></td>
        <td><button class="coupon-button" id="coupon">쿠폰 적용</button>
        <td class="discounted-price-cell" style="display: none;">
            <span class="discounted-price" data-discount="0"></span>
            <button class="change-button">변경</button>
        </td>
        <td><button class="del">삭제</button></td>
    </tr>
    </tbody>
</table>

<!-- 팝업 -->
<div id="coupon-popup" class="popup hidden">
    <div class="popup-header">
        <span>쿠폰 적용</span>
        <button id="close-popup" class="close-btn">×</button>
    </div>
    <div class="popup-body">
        <div id="coupon-list">
            <!-- 쿠폰 리스트가 여기에 동적으로 삽입됩니다. -->
        </div>
        <div id="discounted-price">
            할인된 가격: <span id="price" class="discounted-price-value"></span>
        </div>
        <button class="apply-coupon">적용하기</button>
    </div>
</div>

<h3>총 합계: <span id="totalAmount" th:text="${total}"></span></h3>
<button type="button" id="buy">결제하기</button>
</body>
</html>
$(document).ready(function() {
    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 = parseFloat($(this).find('.discounted-price').text().replace('원', ''));
            else price = parseFloat($(this).find('td:nth-child(3)').text().replace('원', ''));

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

    // 수량 증가 버튼 클릭 이벤트 처리
    $('.increase-quantity').on('click', function() {
        updateQuantity(this, 1);
    });

    // 수량 감소 버튼 클릭 이벤트 처리
    $('.decrease-quantity').on('click', function() {
        updateQuantity(this, -1);
    });

    function updateQuantity(button, increment) {
        var $button = $(button);
        var $row = $button.closest('tr');
        var cartItemId = $button.attr('data-cart-item-id');
        var $quantitySpan = $row.find('.quantity');
        var currentQuantity = parseInt($quantitySpan.text());
        var newQuantity = currentQuantity + increment;

        // 수량이 1 이하로 떨어지지 않도록 합니다.
        if (newQuantity < 1) {
            alert('수량은 1보다 작을 수 없습니다.');
            return;
        }

        $.post('/cart/update', { cartItemId: cartItemId, quantity: newQuantity }, function(response) {
            if (response === 'success') {
                var $originPriceSpan = $row.find('.origin-price');
                var originPrice = parseFloat($originPriceSpan.text());
                var totalPrice = newQuantity * originPrice;
                const discountPercent = parseInt($row.find('.discounted-price').attr('data-discount'), 10) || 0;
                var discountPrice = totalPrice*(1-(discountPercent/100));
                // 수량과 총 가격 업데이트
                $quantitySpan.text(newQuantity);
                $row.find('.total-price').text(totalPrice + '원');
                //console.log('Updated total price:', totalPrice);

                if(discountPercent> 0){
                    $row.find('.discounted-price').text(discountPrice + '원')
                    console.log('Updated total price:', discountPrice);
                }

                updatePriceAndTotal();
            } else {
                alert('Failed to update cart item quantity.');
            }
        });
    }

    // 초기 로딩 시 가격 업데이트
    updatePriceAndTotal();

    //장바구니 삭제 코드
    const del = document.querySelectorAll(".del");
    let id = null;
    del.forEach(button => {
        button.addEventListener('click', function() {
            const row = button.closest('tr');
            id = row.getAttribute('data-cart-item-id');

            if (id) {
                const url = new URL('http://localhost:8080/cart/delete');
                url.searchParams.append('product_id', id);

                fetch(url, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded',
                        'Accept': 'application/json'
                    }
                })
                    .then(response => response.text())
                    .then(data => {
                        if(data === "success") updatePriceAndTotal();
                    })
                    .catch(error => {
                        console.error('Error:', error);
                        alert('There was an error with the post request.');
                    });
            }
        });
    });

    // 팝업과 관련된 요소들
    const popup = document.getElementById('coupon-popup');
    const closePopupButton = document.getElementById('close-popup');
    const couponList = document.getElementById('coupon-list');
    const priceElement = document.getElementById('price');

    // 각 쿠폰 적용 버튼에 이벤트 리스너 추가
    document.querySelectorAll('.coupon-button').forEach(button => {
        button.addEventListener('click', (event) => {
            const row = event.target.closest('tr'); // 버튼이 포함된 행 찾기
            const productPrice = parseFloat(row.querySelector('.origin-price').textContent);
            const quantity = parseInt(row.querySelector('.quantity').textContent, 10);
            const totalPrice = productPrice * quantity;
            const applyButton = popup.querySelector('.apply-coupon');
            const discountedPriceElement = popup.querySelector('.discounted-price-value');
            const changeButton = row.querySelector('.change-button');
            // 팝업에 현재 가격 설정
            priceElement.textContent = `${totalPrice.toFixed(0)}`;

            // 쿠폰 리스트 동적 로딩
            fetch('/getCoupon')
                .then(response => response.json())
                .then(coupons => {
                    couponList.innerHTML = ''; // 이전 내용 지우기
                    coupons.forEach(coupon => {
                        console.log(coupon);
                        const label = document.createElement('label');
                        label.innerHTML = `
                            <input type="radio" name="coupon" value="${coupon.percent}" class="coupon-option">
                             ${coupon.percent}% 할인 쿠폰
                        `;
                        couponList.appendChild(label);
                    });
                });

            // 쿠폰 선택 시 가격 업데이트
            document.addEventListener('change', (e) => {
                if (e.target.classList.contains('coupon-option')) {
                    const discount = parseInt(e.target.value, 10);
                    const discountedPrice = totalPrice * (1 - discount / 100);
                    priceElement.textContent = `${discountedPrice.toFixed(0)}`;
                }
            });

            // 변경 버튼 클릭 시 처리 (예: 다시 쿠폰 적용)
            changeButton.addEventListener('click', () => {
                    popup.style.display = "flex";
            });

            // 쿠폰 적용 버튼 클릭 시 할인된 가격 계산
            applyButton.addEventListener('click', () => {
                const selectedCoupon = popup.querySelector('input[name="coupon"]:checked');
                if (selectedCoupon) {
                    const discountPercent = parseInt(selectedCoupon.value, 10);
                    const discountedPrice = totalPrice * (1 - discountPercent / 100);

                    // 할인된 가격을 테이블에 표시
                    const discountedPriceCell = row.querySelector('.discounted-price-cell');
                    const discountPriceSpan = discountedPriceCell.firstElementChild;
                    discountPriceSpan.textContent = `${discountedPrice.toFixed(0)}`;
                    discountPriceSpan.setAttribute('data-discount', discountPercent.toString()); // 할인율 저장
                    discountedPriceCell.style.display = 'table-cell';

                    updatePriceAndTotal();

                    // 팝업 숨기기
                    popup.style.display = "none";
                }
            });

            // 팝업 표시
            popup.style.display="flex";
        });
    });

    // 팝업 숨기기
    closePopupButton.addEventListener('click', () => {
        popup.style.display = "none";
    });


    // 팝업 이동 가능하도록 하기
    makeDraggable(popup);

    // 팝업 이동 가능 함수
    function makeDraggable(element) {
        let offsetX, offsetY, isDragging = false;

        element.addEventListener('mousedown', (e) => {
            isDragging = true;
            offsetX = e.clientX - element.getBoundingClientRect().left;
            offsetY = e.clientY - element.getBoundingClientRect().top;
        });

        document.addEventListener('mousemove', (e) => {
            if (isDragging) {
                element.style.left = `${e.clientX - offsetX}px`;
                element.style.top = `${e.clientY - offsetY}px`;
            }
        });

        document.addEventListener('mouseup', () => {
            isDragging = false;
        });
    }

});

여기서 아직 버그가 있는데, 적용하기를 누른 후, 수량을 변경하고 변경버튼을 클릭하면, 변경된 수량으로 계산된 가격이 바로 갱신이 되지 않아서 가격에서 오류가 뜬다. 또한, 쿠폰을 현재는 중복해서 적용할 수 있다는 버그가 있다. 그외에는 수정하엿다.

테스트 결과

이제 남은 버그들을 수정하고 결제기능만 만들면 얼추 쇼핑몰의 기능들은 모두 구현을 해본 것같다..

profile
모르는 것 정리하기

0개의 댓글