이전 글에서 말했듯이 이번 시간에는 할인율을 적용할 것이다.
할인율은 다음 정책으로 만들기로 했다.
할인율을 사이트 담당자가 정함. 담당자가 할인율을 책정하여 적용하면 해당 상품이 퍼센트치 만큼 할인됨.
그래서 먼저, 관리자만 이용할 수 있는 사이트가 필요하다. 그러니 스프링 시큐리티에서 관리자만 사용할 수 있도록 인가에 조건을 걸었다.
.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이면 해당 값은 가져오지 않는 것이다.
또한 +'%'를 이용하여 해당 테스트값 뒤에 %를 붙일 수 있다.
테스트 결과
다음과 같이 할인율, 원래가격, 현재가격을 볼 수 있다. 실험용 하의는 할인을 받지 않았기 때문에 할인과 관련된 값이 표시되지 않음을 확인할 수 있다.
이제 상세정보창에도 같은 방법으로 할인율을 적용해주고, 다음 시간에는 쿠폰 기능을 만들어보자.
이제 진짜 얼마 남지 않았다. 물론 허술하고, 맨땅에 헤딩하듯이 프로젝트를 하고있긴하지만 어느정도 있어야할 기능들은 구현에 성공한 것 같다.