쇼핑몰 웹사이트 만들어보기 - 장바구니 기능 구현

Shiba·2024년 7월 27일
0

프로젝트 및 일기

목록 보기
9/29

이번 시간에는 장바구니 기능을 구현해보도록 하자.

먼저, 장바구니 테이블을 데이터베이스에 추가해주자

create table cart (
	id int not null auto_increment,
    userId varchar(40) not null,
    productId int not null,
    quantity int not null
    
    primary key(id),
    foreign key(userId) references users(id),
    foreign key(productId) references products(id)
);

테이블을 생성했으니 엔티티를 만들고, SpringMVC를 이용하여 기능을 구현해보자

@Entity
public class Cart {

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

    private String userId;
    
    @ManyToOne
    @JoinColumn(name = "userId", insertable = false, updatable = false)
    private Users user;
    
    private int productId;
    
    @ManyToOne
    @JoinColumn(name = "productId", insertable = false, updatable = false)
    private Products products;

    private int quantity;
    
    //getter setter 생략//
}

장바구니에는 어떤 기능이 있어야 할까? 장바구니에 상품을 추가하는 기능이 필요할 것이고, 장바구니를 보았을 때, 장바구니에 넣어둔 상품들이 보여져야할 것이다.

그러니 addCart()와 getCart()메소드를 만들어서 작동되도록하면 될 것같다.

cartRepository

import com.shoppingmall.domain.Cart;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;

import java.util.List;

public class CartRepository {
    @PersistenceContext
    private final EntityManager em;

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

    public void addCart(Cart cart){
        em.persist(cart);
    }

    public List<Cart> getCart(String userId){
        TypedQuery<Cart> query = em.createQuery("SELECT u FROM Cart u WHERE u.userId = :userId", Cart.class);
        query.setParameter("userId",  userId);
        return query.getResultList();
    }
}

cartService

import com.shoppingmall.domain.Cart;
import com.shoppingmall.domain.Products;
import com.shoppingmall.domain.Users;
import com.shoppingmall.repository.CartRepository;
import com.shoppingmall.repository.ProductRepository;
import com.shoppingmall.repository.UserRepository;

import java.util.List;

public class CartService {

    private final CartRepository cartRepository;
    private final UserRepository userRepository;
    private final ProductRepository productRepository;
    public CartService(CartRepository cartRepository, UserRepository userRepository, ProductRepository productRepository) {
        this.cartRepository = cartRepository;
        this.userRepository = userRepository;
        this.productRepository = productRepository;
    }

    public void addCart(String userId, int productId, int quantity){
        Users user = userRepository.findById(userId);
        Products products = productRepository.findById(productId);
        // 장바구니 정보
        Cart cart = new Cart();
        cart.setUser(user);
        cart.setUserId(user.getId());
        cart.setProducts(products);
        cart.setProductId(products.getId());
        cart.setQuantity(quantity);

        //저장
        cartRepository.addCart(cart);
    }

    public List<Cart> getCart(String userId){
        return cartRepository.getCart(userId);
    }
}

이제 컨트롤러부분을 작성해주자. 일단, 상품을 클릭했을 때, 해당 상품의 상세정보를 볼 수 있도록 하는 페이지를 구성하자. 그 페이지에서 장바구니 담기버튼을 만들고 버튼을 클릭 시 장바구니에 담겼다는 알림이 뜨도록 해보자

@GetMapping("/products/{id}")
    public String getProductDetail(@PathVariable("id") int id,
                                   @RequestParam(value = "query", required = false) String keyword,
                                   @RequestParam(value = "category", required = false) String category,
                                   @RequestParam(value = "sellerId", required = false) String sellerId,
                                   Model model) {
        Products product = productService.findById(id);
        model.addAttribute("product", product);
        model.addAttribute("query", keyword); // 검색 쿼리를 모델에 추가
        model.addAttribute("category", category); // 검색 쿼리를 모델에 추가
        model.addAttribute("sellerId", sellerId); // 검색 쿼리를 모델에 추가

        return "product/productDetail"; // 상세 페이지의 템플릿 이름
    }

위 컨트롤러를 통해 상세페이지로 이동이 가능하다.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Fast Mall - 상품 상세 정보</title>
    <script>
        function handleSubmit(event) {
            event.preventDefault(); // 기본 폼 제출 동작 방지
            const form = event.target;

            // 폼 데이터를 객체로 생성
            const formData = new FormData(form);

            // Fetch API를 사용하여 비동기 요청 보내기
            fetch(form.action, {
                method: form.method,
                body: formData,
                headers: {
                    'X-Requested-With': 'XMLHttpRequest'
                }
            })
                .then(response => response.text())
                .then(data => {
                    if (data === 'success') {
                        if (data === 'success') {
                            alert('상품이 장바구니에 추가되었습니다.');
                        } else if (data === 'not_logged_in') {
                            alert('로그인을 해주세요.');
                        } else {
                            alert('상품 추가에 실패했습니다.');
                        }
                    }
                })
                .catch(error => {
                    console.error('Error:', error);
                    alert('상품 추가에 실패했습니다.');
                });
        }

        function incrementValue() {
            let value = parseInt(document.getElementById('quantity').value, 10);
            value = isNaN(value) ? 0 : value;
            value++;
            document.getElementById('quantity').value = value;
            updateTotalPrice();
        }

        function decrementValue() {
            let value = parseInt(document.getElementById('quantity').value, 10);
            value = isNaN(value) ? 0 : value;
            value--;
            value = value < 1 ? 1 : value; // 최소 수량 1로 설정
            document.getElementById('quantity').value = value;
            updateTotalPrice();
        }

        function updateTotalPrice() {
            const price = parseFloat(document.getElementById('price').textContent);
            const quantity = parseInt(document.getElementById('quantity').value, 10);
            const totalPrice = price * quantity;
            document.getElementById('totalPrice').textContent = totalPrice.toString();
        }
    </script>
</head>
<body>
<div class="header">
    <a href="/" id="home_logo">
        <img src="/images/icons/logo.png" alt="Home Logo"/>
    </a>
</div>
<h1>상품 상세 정보</h1>
<div>
    <h2 th:text="${product.product_name}"></h2>
    <img th:src="@{'/images/uploads/' + ${product.photo}}" alt="Product Image" height="400" width="400">
    <p>판매자: <span th:text="${product.seller_id}"></span></p>
    <p>설명: <span th:text="${product.description}"></span></p>
    <p>가격: <span id="price" th:text="${product.price}"></span></p>
</div>
<form th:action="@{/cart/add}" method="post" onsubmit="handleSubmit(event)">
    <input type="hidden" name="productId" th:value="${product.id}"/>
    <input type="hidden" id="query" name="query" th:value="${query}"/>
    <input type="hidden" id="category" name="category" th:value="${category}"/>
    <input type="hidden" id="sellerId" name="sellerId" th:value="${sellerId}"/>
    <div>
        <button type="button" onclick="decrementValue()">-</button>
        <input type="number" id="quantity" name="quantity" value="1" readonly />
        <button type="button" onclick="incrementValue()">+</button>
    </div>
    <p>결제 가격: <span id="totalPrice" th:text="${product.price}"></span></p>
    <button type="submit">장바구니 담기</button>
</form>
<a th:href="@{/search(query=${query}, category=${category}, sellerID=${sellerId})}" style="color: #3180d1">검색 결과로 돌아가기</a>
</body>
</html>

상세페이지를 구현했으니 테스트를 해보자

테스트를 해보니 상세페이지까지는 정상출력이 되지만, 장바구니 담기를 클릭시 sql오류가 뜬다. product_id라는 컬럼을 찾지못했다는 오류이다
계속 만져보니 아무리봐도 sql에서 대문자로 해당 변수를 표시하면 인식을 못하는 느낌인거 같다. userId를 user_id로, productId를 product_id로 고치니 정상적으로 실행이 되었다.

장바구니 테이블에도 성공적으로 추가가 된 모습이다

여담) 계속 하면서 느끼는 점이 기능이 늘어날수록 위와같이 직접 서버를 열어서 실행하기에는 무리가 있을 수도 있다는 생각이 들었다.
이래서 토비님이 tdd를 통해서 만들면 테스트하기 변하다고 하셨던건가 싶다. 지금은 서버를 여는데에 10초이지만, 실무에서는 아마 더 한 시간이 걸릴 것이고, 지금처럼 계속 서버를 열다간 테스트한번에 모든 시간이 가버릴 것같았다. 그래서 지금부터라도 테스트를 작성하는 연습을 해보아야겠다.

profile
모르는 것 정리하기

0개의 댓글