쇼핑몰 웹사이트 만들어보기 - 상품 할인율 적용

Shiba·2024년 8월 8일
0

프로젝트 및 일기

목록 보기
15/29

이전 글에서 말했듯이 이번 시간에는 할인율을 적용할 것이다.

할인율은 다음 정책으로 만들기로 했다.

할인율을 사이트 담당자가 정함. 담당자가 할인율을 책정하여 적용하면 해당 상품이 퍼센트치 만큼 할인됨.

그래서 먼저, 관리자만 이용할 수 있는 사이트가 필요하다. 그러니 스프링 시큐리티에서 관리자만 사용할 수 있도록 인가에 조건을 걸었다.

.authorizeHttpRequests((requests)->requests
                        .requestMatchers("/css/**", "/js/**", "/images/**", "/json/**").permitAll() // CSS 파일에 대한 접근을 허용
                        .requestMatchers("/user/status", "/products/add", "/cart/**", "/seller**").authenticated()
                        .requestMatchers("/admin/**").hasRole("admin")
                        .anyRequest().permitAll())
                .exceptionHandling(ex -> ex
                .accessDeniedPage("/") // 접근 거부 페이지 설정
                )

이렇게 해두면 사용자가 만약에 관리자 페이지에 접근하려 한다면 시작 페이지("/")로 강제 이동된다.

다음으로 할인율을 저장해두기 위해 Products 테이블을 수정했다. discount라는 열을 하나 추가하여 여기에 할인율을 저장할 수 있도록 하였다.

//엔티티에도 다음과 같이 추가
 @Column(name = "discount")
    private double discount;
    
    
//getter and setter

이제 할인율관련 메소드를 작성해보자. 상품 각각마다 할인율이 지정될 것이고, 지정된 할인율을 저장할 것이다.

MemoryProductRepository

	@Override
    public boolean discount(int id, int discount) {
        Products product = em.find(Products.class, id);
        if (product != null) {
            product.setDiscount(discount);
            // `merge` 메서드를 사용하여 업데이트 수행
            em.merge(product);
            return true;
        }
        return false;
    }
    
	@Override
    public List<Products> getAllProduct() { //관리자페이지에서 모든 상품을 보기위해 추가
        String query = "SELECT c FROM Products c";
        return em.createQuery(query, Products.class).getResultList();
    }

ProductService

public boolean discount(int id, int discount) { return productRepository.discount(id, discount); }
public List<Products> getAllProduct() { return productRepository.getAllProduct(); }

ProductController

@GetMapping("/admin/product")
    public String adminPage(Model model){
        List<Products> productsList = new ArrayList<>();
        productsList = productService.getAllProduct();
        List<ProductDto> productDtos = new ArrayList<>();
        for (Products product:productsList) {
            int price = product.getPrice();
            int discountPrice = (int) (price * (1-product.getDiscount()/100));
            productDtos.add(new ProductDto(product,discountPrice));
        }
        model.addAttribute("productDto", productDtos);
        return "/user/admin";
    }

    @PostMapping("/products/discount")
    public ResponseEntity<String> discountProduct(@RequestParam(name = "product_id") int product_id,
                                                  @RequestParam(name = "discount") int discount){
        ResponseEntity response;
        try {
            if(productService.discount(product_id, discount)) {
                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;
    }

우리는 할인율만 저장하므로 할인된 가격을 보여주기 위해서 서버에서 미리 계산하여 보내는 방식을 사용했다.

그래서 ProductDto라는 dto클래스를 만들어 사용했다.

ProductDto

package com.shoppingmall.dto;

import com.shoppingmall.domain.Products;

public class ProductDto {

    private Products products;
    private int discountPrice;

    public ProductDto(Products product, int discountPrice) {
        this.products = product;
        this.discountPrice = discountPrice;
    }

    public Products getProducts() {
        return products;
    }

    public void setProducts(Products products) {
        this.products = products;
    }

    public int getDiscountPrice() {
        return discountPrice;
    }

    public void setDiscountPrice(int discountPrice) {
        this.discountPrice = discountPrice;
    }
}

이제 admin페이지만 만들면 끝이다. admin페이지는 그동안 만들었던 페이지들을 짜집기해서 만들었다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="/css/admin.css">
    <title>관리자 전용 페이지</title>
    <script>
        document.addEventListener("DOMContentLoaded", function () {
            const popup = document.getElementById("modPopup");
            const discount = document.querySelectorAll(".discount");
            const closePopup = document.getElementById('close-popup');
            const popupContent = document.getElementById('popup-content');
            const postButton = document.getElementById('mod_submit');
            const discountPriceInput = document.getElementById("discountPrice");
            const updatedPrice = document.getElementById("updatedPrice");

            let currentId = null;
            let originalPrice = null;
            let nowPrice = null;
            discount.forEach(button => {
                button.addEventListener('click', function() {
                    const row = button.closest('tr');
                    const name = row.cells[0].innerText;
                    originalPrice = parseFloat(row.cells[2].innerText.replace(/[^\d.-]/g, '')); // 숫자만 추출
                    nowPrice = parseFloat(row.cells[3].innerText.replace(/[^\d.-]/g, '')); // 숫자만 추출
                    currentId = row.getAttribute('data-product-id');

                    popupContent.innerText = `상품 명: ${name}, 원래 가격: ${originalPrice}, 현재 가격: ${nowPrice}`;
                    updatedPrice.innerText = `변경된 가격: ${originalPrice}`; // 초기 표시 가격
                    popup.style.display = 'block';
                });
            });

            // 할인율 입력 필드의 값이 변경될 때마다 실행
            discountPriceInput.addEventListener('input', function() {
                if (originalPrice !== null) {
                    const discountPercentage = parseFloat(discountPriceInput.value) || 0;
                    const discounted = 1-(discountPercentage/100);
                    const discountedPrice = originalPrice * discounted;
                    updatedPrice.innerText = `변경된 가격: ${discountedPrice.toFixed(0)}`;
                }
            });

            postButton.addEventListener('click', function() {
                if (currentId && discountPriceInput.value) {
                    const url = new URL('http://localhost:8080/products/discount');
                    url.searchParams.append('product_id', currentId);
                    url.searchParams.append('discount', discountPriceInput.value);

                    fetch(url, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/x-www-form-urlencoded',
                            'Accept': 'application/json'
                        }
                    })
                        .then(response => response.text())
                        .then(data => {
                            alert(data);
                            popup.style.display = 'none';
                            discountPriceInput.value = ''; // 입력 필드를 초기화
                            updatedPrice.innerText = '변경된 가격: '; // 가격 초기화
                            window.location.href = "/admin/product";
                        })
                        .catch(error => {
                            console.error('Error:', error);
                            alert('There was an error with the post request.');
                        });
                }
            });

            closePopup.onclick = function() {
                discountPriceInput.value = ''; // 입력 필드를 초기화
                updatedPrice.innerText = '변경된 가격: '; // 가격 초기화
                popup.style.display = "none";
            }

            window.onclick = function(event) {
                if (event.target == popup) {
                    discountPriceInput.value = ''; // 입력 필드를 초기화
                    updatedPrice.innerText = '변경된 가격: '; // 가격 초기화
                    popup.style.display = "none";
                }
            }
        });
    </script>
</head>
<body>
<table>
    <thead>
    <tr>
        <th>상품명</th>
        <th>할인 적용하기</th>
        <th>원래 가격</th>
        <th>할인된 가격</th>
    </tr>
    </thead>
    <tbody>
    <tr class="clickable-row" th:each="product : ${productDto}" th:data-product-id="${product.products.id}">
        <td id="product"><img th:src="@{'/images/uploads/' + ${product.products.photo}}" alt="Product Image" height="80" width="80">
            <span id="name" th:text="${product.products.product_name}"></span></td>
        <td><span>할인율 : </span><span id="percent" th:text="${product.products.discount}">%</span> </span><button class="discount">할인 적용하기</button></td>
        <td th:text="${product.products.price}"></td>
        <td><span th:text="${product.discountPrice}"></span></td>
    </tr>
    </tbody>
</table>

<div id="modPopup" class="popup">
    <div class="popup-content">
        <button id="close-popup">Close</button>
        <h2>Settings</h2>
        <!-- Add your settings options here -->
        <p id="popup-content"></p>
        <div id="productForm">
            <label for="discountPrice">할인율 : </label>
            <input id="discountPrice" type="number" name="discountPrice" placeholder="Product Price" step="1" required>
            <p id="updatedPrice">변경된 가격: </p>
            <button id="mod_submit">할인율 적용</button>
        </div>
    </div>
</div>

</body>
</html>

지난시간에서 사용했던 팝업을 들고와서 페이지에서 원하는 기능을 수행하도록 수정했다. 할인율을 입력하면 label의 가격이 변경되어 보여지게된다.

테스트 결과

정상적으로 실행됨을 확인할 수 있었다.

이제 이 할인율을 상품 검색창에서도 보이게 해보자.

그러기위해서는 컨트롤러부분과 프론트코드만 수정해주면된다.

SearchController

@GetMapping("/search")
    public String search( @RequestParam(value = "query", required = false) String keyword,
                          @RequestParam(value = "category", required = false) String category,
                          @RequestParam(value = "sellerId", required = false) String sellerId,
                          Model model) {
        // 여기서 query는 요청 파라미터의 이름입니다.
        // 예를 들어, /search?query=검색어 형식으로 요청이 들어온다면,
        // "검색어" 부분이 query 매개변수로 전달됩니다.

        List<Products> products = new ArrayList<>();

        if (category != null && !category.isEmpty()) {
            products = productService.searchByCategory(category);
        } else if (sellerId != null) {
            products = productService.searchBySellerId(sellerId);
        }else products = productService.searchByName(keyword);
        List<ProductDto> productDtos = new ArrayList<>();
        for (Products product:products) {
            int price = product.getPrice();
            int discountPrice = (int) (price * (1-product.getDiscount()/100));
            productDtos.add(new ProductDto(product,discountPrice));
        }
        model.addAttribute("query", keyword);
        model.addAttribute("category", category);
        model.addAttribute("sellerId", sellerId);
        model.addAttribute("productDto", productDtos);
        // 검색결과를 표시할 템플릿 이름을 반환합니다.
        return "searchResult"; // search-result.html과 같은 템플릿 파일을 찾게 됩니다.
    }

검색 컨트롤러에서 원래는 Product 리스트를 받았다면 이를 Dto로 바꾼다.

또한 위의 테스트 결과에서 할인율이 x.0의 꼴로 나오는게 보기 싫어서 Dto도 조금 수정했다.

Dto 수정

package com.shoppingmall.dto;

import com.shoppingmall.domain.Products;

public class ProductDto {

    private Products products;

    private int percent;
    private int discountPrice;

    public ProductDto(Products product, int discountPrice) {
        this.products = product;
        this.percent = (int) product.getDiscount();
        this.discountPrice = discountPrice;
    }

    public Products getProducts() {
        return products;
    }

    public void setProducts(Products products) {
        this.products = products;
    }

    public int getDiscountPrice() {
        return discountPrice;
    }

    public void setDiscountPrice(int discountPrice) {
        this.discountPrice = discountPrice;
    }

    public int getPercent() {
        return percent;
    }

    public void setPercent(int percent) {
        this.percent = percent;
    }
}

할인율을 정수로 받아서 이를 바로 타임리프를 이용해 웹페이지에 보여줄 것이다.

searchResult.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Fast Mall - 검색결과</title>
    <link rel="stylesheet" href="/css/searchResult.css">
    <script>
        document.addEventListener('DOMContentLoaded', function () {
            // 모든 테이블 행에 대해 클릭 이벤트를 추가
            const rows = document.querySelectorAll('.clickable-row');
            rows.forEach(row => {
                row.addEventListener('click', function () {
                    const productId = this.dataset.productId;
                    // 상품 상세 페이지로 이동
                    window.location.href = `/products/${productId}`;
                });
            });
        });
    </script>
</head>
<body>
<div class="header">
    <a href="/" id="home_logo">
        <img src="/images/icons/logo.png" alt="Home Logo"/>
    </a>
</div>
<form action="/search">
    <!-- 타임리프를 통해 query값을 매핑해서 화면에 보여주기 -->
    <h1><span style="color: #3180d1" th:text="${query}">
    </span><span style="color: #3180d1" th:text="${category}">
    </span><span style="color: #3180d1" th:text="${sellerId}"></span> 검색결과</h1>
    <table>
        <tbody>
        <tr class="clickable-row" th:each="product : ${productDto}" th:data-product-id="${product.products.id}">
            <td><img id="image" th:src="@{'/images/uploads/' + ${product.products.photo}}" alt="Product Image">
                <div id="box"><span id="name" th:text="${product.products.product_name}"></span>
                    <div>
                        <span id="percent" th:if="${product.percent > 0}" th:text="${product.percent} + '%'"></span>
                        <span id="price" th:if="${product.products.price != product.discountPrice}"  th:text="${product.products.price} + '원'"></span>
                        <span id="discount_price" th:text="${product.discountPrice} + '원'"></span>
                    </div>
                </div></td>
        </tr>
        </tbody>
    </table>
</form>
</body>
</html>

여기서 처음 알았는데 th:if를 이용하면 받아온 값에 대해 조건문을 사용할 수 있고, 조건문이 만족할 때만 th:text의 값을 가지게된다. 즉, percent가 0이면 해당 값은 가져오지 않는 것이다.

또한 +'%'를 이용하여 해당 테스트값 뒤에 %를 붙일 수 있다.

테스트 결과

다음과 같이 할인율, 원래가격, 현재가격을 볼 수 있다. 실험용 하의는 할인을 받지 않았기 때문에 할인과 관련된 값이 표시되지 않음을 확인할 수 있다.

이제 상세정보창에도 같은 방법으로 할인율을 적용해주고, 다음 시간에는 쿠폰 기능을 만들어보자.

이제 진짜 얼마 남지 않았다. 물론 허술하고, 맨땅에 헤딩하듯이 프로젝트를 하고있긴하지만 어느정도 있어야할 기능들은 구현에 성공한 것 같다.

profile
모르는 것 정리하기

0개의 댓글