오늘은 쿠폰 기능을 적용해보았다. 아직 자잘한 버그가 존재하지만 어느정도 윤곽은 잡힌 것 같다.
먼저, 쿠폰 데이터베이스 테이블을 만들고, 쿠폰의 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;
});
}
});
여기서 아직 버그가 있는데, 적용하기를 누른 후, 수량을 변경하고 변경버튼을 클릭하면, 변경된 수량으로 계산된 가격이 바로 갱신이 되지 않아서 가격에서 오류가 뜬다. 또한, 쿠폰을 현재는 중복해서 적용할 수 있다는 버그가 있다. 그외에는 수정하엿다.
테스트 결과
이제 남은 버그들을 수정하고 결제기능만 만들면 얼추 쇼핑몰의 기능들은 모두 구현을 해본 것같다..