반응형
백엔드 과제 제출로 오랜만에 처음부터 세팅해서 만져본 스프링부트 + JPA
퇴사하고 약 4달만에 코딩해보는거라 처음부터 약간 버벅거렸지만..
기록해보겠습니당.
- 개발 환경
- Java 21
- Spring Boot 3.4.1
- thymeleaf
- MariaDB 10.10
- build.gradle 의존성
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
implementation 'com.googlecode.json-simple:json-simple:1.1'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation group: 'org.springframework', name: 'spring-jdbc', version: '6.1.12'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf', version: '3.3.5'
- DB
장바구니, 장바구니 상품, 상품, 고객, 주문(결제) 테이블로 구성.
상품 목록 조회
1. 프론트는 부트스트랩 쇼핑몰 템플릿을 사용했습니다.
https://themewagon.com/themes/foodmart/
2. 상품 Entity
@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Product extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_id")
private Long id; //상품 ID
@Column(nullable = false, length = 255)
private String name; //상품 이름
@Column(length = 1000)
private String description; //상품 설명
@Column(nullable = false)
private Long price; //상품 가격
@Column(nullable = false)
private int stock; //재고 수량
@Column(nullable = false)
private String imgUrl; //이미지 경로
public static Product toProduct(ProductDto productDto) {
Product product = new Product();
product.setId(productDto.getId());
product.setName(productDto.getName());
product.setDescription(productDto.getDescription());
product.setPrice(productDto.getPrice());
product.setStock(productDto.getStock());
product.setImgUrl(productDto.getImgUrl());
return product;
}
public static Product uptProduct(ProductDto productDto) {
// 생략
}
}
3. 상품 Dto
@Getter
@Setter
@NoArgsConstructor
@ToString
public class ProductDto {
private Long id;
private String name;
private String description;
private Long price;
private int stock;
private String imgUrl;
public ProductDto(Long id, String name, String description, Long price, int stock, String imgUrl) {
this.id = id;
this.name = name;
this.description = description;
this.price = price;
this.stock = stock;
this.imgUrl = imgUrl;
}
public static ProductDto toProductDto(Product product) {
ProductDto productDto = new ProductDto();
productDto.setId(product.getId());
productDto.setName(product.getName());
productDto.setDescription(product.getDescription());
productDto.setPrice(product.getPrice());
productDto.setStock(product.getStock());
productDto.setImgUrl(product.getImgUrl());
return productDto;
}
}
4. 전체 상품 조회
Repository
public interface ProductRepository extends JpaRepository<Product, Long>{
Product findProductById(Long id);
}
Service
/*
* DTO -> Entity
* Entity -> DTO
*/
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
public List<ProductDto> getAllProduct() {
List<Product> productList = productRepository.findAll();
List<ProductDto> dtoList = new ArrayList<>();
for (Product product : productList) {
dtoList.add(ProductDto.toProductDto(product));
}
return dtoList;
}
public ProductDto findByProductId(Long id) {
// 생략
}
}
5. 전체 상품 조회 후 메인 화면에 전달
Controller
@Controller
@RequiredArgsConstructor
public class MainController {
private final ProductService productService;
@GetMapping("/")
public String main(Model model) {
List<ProductDto> products = productService.getAllProduct();
model.addAttribute("products", products);
return "index";
}
}
6. 메인 화면 index.html
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/layout}">
<th:block layout:fragment="script">
<script th:inline="javascript">
// 생략
</script>
</th:block>
<th:block layout:fragment="content">
<section class="py-5">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="bootstrap-tabs product-tabs">
<div class="tabs-header d-flex justify-content-between border-bottom my-5">
<h3>상품목록</h3>
<nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<a href="#" class="nav-link text-uppercase fs-6 active" id="nav-all-tab" data-bs-toggle="tab" data-bs-target="#nav-all">All</a>
<!-- <a href="#" class="nav-link text-uppercase fs-6" id="nav-fruits-tab" data-bs-toggle="tab" data-bs-target="#nav-fruits">Fruits & Veges</a>
<a href="#" class="nav-link text-uppercase fs-6" id="nav-juices-tab" data-bs-toggle="tab" data-bs-target="#nav-juices">Juices</a> -->
</div>
</nav>
</div>
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-all" role="tabpanel" aria-labelledby="nav-all-tab">
<div class="product-grid row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5">
<th:block th:each="products : ${products}">
<input type="hidden" id="productId" th:value="${products.id}">
<div class="col">
<div class="product-item">
<figure>
<a href="index.html" title="Product Title">
<img th:src="${products.imgUrl}" class="tab-image">
</a>
</figure>
<h3>[[${products.name}]]</h3>
<!-- <span class="qty" id="stock">재고 : [[${products.stock}]]개</span> -->
<span class="price">[[${products.price}]]</span>
<div class="d-flex align-items-center justify-content-between">
<div class="input-group product-qty">
<span class="input-group-btn">
<button type="button" class="quantity-left-minus btn btn-danger btn-number" data-type="minus">
<!-- <svg width="16" height="16"><use xlink:href="#minus"></use></svg> -->
<i class="fa-solid fa-minus"></i>
</button>
</span>
<input type="text" id="quantity" name="quantity" class="form-control input-number" value="1">
<span class="input-group-btn">
<button type="button" class="quantity-right-plus btn btn-success btn-number" data-type="plus">
<!-- <svg width="16" height="16"><use xlink:href="#plus"></use></svg> -->
<i class="fa-solid fa-plus"></i>
</button>
</span>
</div>
<button type="button" class="btn btn-light border border-primary btn-lg btn-cart-font" th:attr="onclick=|addCart(${products.id})|">장바구니 담기 <i class="fa-solid fa-cart-arrow-down"></i></button>
</div>
</div>
</div>
</th:block>
</div>
<!-- / product-grid -->
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</th:block>
</html>
장바구니 담기 기능
1. 장바구니, 장바구니 상품 Entity
회원 1명당 1개의 장바구니를 갖는다.
@Entity
@Getter
@Setter
@ToString
public class Cart extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cart_id")
private Long id;
private int count; // 카트 상품 개수
@OneToOne(fetch = FetchType.EAGER)
@JoinColumn(name="customer_id")
private Customer customer;
@OneToMany(mappedBy = "cart", cascade = CascadeType.PERSIST)
private List<CartProduct> cartList = new ArrayList<>();
public static Cart createCart(Customer customer) {
Cart cart = new Cart();
cart.setCount(0);
cart.setCustomer(customer);
return cart;
}
}
@Entity
@Getter
@Setter
public class CartProduct extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cart_product_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="cart_id")
private Cart cart;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="product_id")
private Product product;
private int count; // 카트 상품 개수
public static CartProduct createCartProduct(Cart cart, Product product, int count) {
// 생략
}
public void addCount(int count) {
this.count += count;
}
}
2. Dto
@Getter
@Setter
public class CartProductDto {
@NotNull
private Long productId;
@Min(value = 1, message = "최소 1개 이상 담아주세요.")
private int count;
}
@Getter
@Setter
public class CartDetailDto {
private Long cartProductId;
private String name;
private Long price;
private int count;
private String imgUrl;
public CartDetailDto(Long cartProductId, String name, Long price, int count, String imgUrl) {
this.cartProductId = cartProductId;
this.name = name;
this.price = price;
this.count = count;
this.imgUrl = imgUrl;
}
}
3. 장바구니 담기
Service
@Service
@RequiredArgsConstructor
@Transactional
public class CartService {
private final CartRepository cartRepository;
private final ProductRepository productRepository;
private final CartProductRepository cartProductRepository;
private final CustomerRepository customerRepository;
public Long addCart(CartProductDto dto, String email) {
// 회원의 장바구니 조회
Product product = productRepository.findById(dto.getProductId())
.orElseThrow(EntityNotFoundException::new);
Customer customer = customerRepository.findByEmail(email);
Cart cart = cartRepository.findByCustomerId(customer.getId());
// 장바구니에 상품을 처음 담을 경우 장바구니 엔티티 생성
if (cart == null) {
cart = Cart.createCart(customer);
cartRepository.save(cart);
}
// 현재 상품이 이미 장바구니에 들어가 있는지 조회
CartProduct savedProduct = cartProductRepository.findByCartIdAndProductId(cart.getId(), product.getId());
// 상품이 장바구니에 없다면 카트 상품 생성 후 추가
if (savedProduct != null) {
savedProduct.addCount(dto.getCount());
return savedProduct.getId();
} else {
CartProduct cartProduct = CartProduct.createCartProduct(cart, product, dto.getCount());
cartProductRepository.save(cartProduct);
return cartProduct.getId();
}
}
@Transactional(readOnly = true)
public List<CartDetailDto> getCartList(@Param("email") String email) {
// 생략
}
@Transactional(readOnly = true)
public boolean validateCart(Long cartProductId, String email) {
// 생략
}
public void updateCartCount(Long cartProductId, int count) {
// 생략
}
public void deleteCart(Long cartProductId) {
// 생략
}
}
Controller
@Controller
@RequiredArgsConstructor
public class CartController {
private final CartService cartService;
private final CustomerService customerService;
@PostMapping(value = "/cart")
public @ResponseBody ResponseEntity addCart(@RequestBody @Valid CartProductDto dto, BindingResult bindingResult, Principal principal){
if (bindingResult.hasErrors()) {
StringBuilder sb = new StringBuilder();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
sb.append(fieldError.getDefaultMessage());
}
return new ResponseEntity<String>(sb.toString(), HttpStatus.BAD_REQUEST);
}
String email = principal.getName();
Long cartProductId;
try {
cartProductId = cartService.addCart(dto, email);
} catch(Exception e){
return new ResponseEntity<String>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<Long>(cartProductId, HttpStatus.OK);
}
@GetMapping("/cart")
public String getCartList(Principal principal, Model model) {
// 생략
}
@DeleteMapping(value = "/cart/{cartProductId}")
public @ResponseBody ResponseEntity deleteCart(@PathVariable("cartProductId") Long cartProductId, Principal principal) {
// 생략
}
}
4. 프론트단
function addCart(productId) {
var url = "/cart";
var paramData = {
productId :productId,
count : $("#quantity").val(),
};
var param = JSON.stringify(paramData);
$.ajax({
url: url,
type : "POST",
contentType : "application/json",
data: param,
dataType: "json",
cache: false,
success: function(status) {
alert("장바구니에 상품을 담았습니다.");
location.href="/";
},
error: function(jqXHR, status, error) {
if (jqXHR.status == '401') {
alert('로그인 후 이용해주세요.');
location.href="/customer/login";
} else {
}
}
});
}
장바구니 리스트, 장바구니 삭제 기능
1. 장바구니 리스트 조회, 삭제
Repository
public interface CartProductRepository extends JpaRepository<CartProduct, Long>{
CartProduct findByCartIdAndProductId(Long cartId, Long productId);
@Query("""
select cp
from CartProduct cp join fetch cp.product p
where cp.cart.id = :cartId
order by cp.createdAt desc
""")
List<CartProduct> findCartDetailDtoList(@Param("cartId") Long cartId);
}
public interface CartRepository extends JpaRepository<Cart, Long>{
Cart findByCustomerId(Long customerId);
}
Service
@Transactional(readOnly = true)
public List<CartDetailDto> getCartList(@Param("email") String email) {
List<CartDetailDto> cartDetailDtoList = new ArrayList<>();
Customer customer = customerRepository.findByEmail(email);
Cart cart = cartRepository.findByCustomerId(customer.getId());
if (cart == null) {
return cartDetailDtoList;
}
List<CartProduct> cartProductList = cartProductRepository.findCartDetailDtoList(cart.getId());
cartDetailDtoList = cartProductList.stream()
.map(m -> new CartDetailDto(m.getId(),
m.getProduct().getName(),
m.getProduct().getPrice(),
m.getCount(),
m.getProduct().getImgUrl()))
.collect(Collectors.toList());
return cartDetailDtoList;
}
public void deleteCart(Long cartProductId) {
CartProduct cartProduct = cartProductRepository.findById(cartProductId)
.orElseThrow(EntityNotFoundException::new);
cartProductRepository.delete(cartProduct);
}
Controller
@GetMapping("/cart")
public String getCartList(Principal principal, Model model) {
// 현재 로그인한 사용자의 이메일 정보를 이용하여 장바구니에 담겨있는 상품 조회
List<CartDetailDto> cartList = cartService.getCartList(principal.getName());
Customer customer = customerService.getCustomerInfo(principal.getName());
model.addAttribute("cartList", cartList);
model.addAttribute("customerName", customer.getName());
model.addAttribute("customerEmail", customer.getEmail());
return "/product/cart";
}
@DeleteMapping(value = "/cart/{cartProductId}")
public @ResponseBody ResponseEntity deleteCart(@PathVariable("cartProductId") Long cartProductId, Principal principal) {
if (!cartService.validateCart(cartProductId, principal.getName())) {
return new ResponseEntity<String>("삭제 권한이 없습니다.", HttpStatus.FORBIDDEN);
}
cartService.deleteCart(cartProductId);
return new ResponseEntity<Long>(cartProductId, HttpStatus.OK);
}
2. 프론트단 cartList.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/layout}">
<th:block layout:fragment="script">
// 생략
</th:block>
<th:block layout:fragment="css">
<style>
// 생략
</style>
</th:block>
<th:block layout:fragment="content">
<h3>장바구니 목록</h3>
<div>
<table class="table">
<colgroup>
<col width="15%"/>
<col width="70%"/>
<col width="15%"/>
</colgroup>
<thead>
<tr class="text-center">
<td><input type="checkbox" id="checkall" onclick="chkAll()"/>전체 선택</td>
<td>상품명</td>
<td>금액</td>
</tr>
</thead>
<tbody>
<tr th:each="cartList : ${cartList}">
<td>
<input type="checkbox" name="cartChkbox" th:value="${cartList.cartProductId}" >
</td>
<td>
<div class="proImgDiv imgItem">
<img th:src="${cartList.imgUrl}" class = "rounded proImg" th:alt="${cartList.name}">
</div>
<div class="imgItem">
<span th:text="${cartList.name}" id="productName" class="fs17 font-weight-bold"></span>
<div class="fs17 font-weight-light">
<span class="input-group mt-2">
<span th:id="'price_' + ${cartList.cartProductId}"
th:data-price="${cartList.price}"
th:text="${cartList.price} + '원'" class="align-self-center mr-2">
</span>
<input type="number" name="count" th:id="'count_' + ${cartList.cartProductId}"
th:value="${cartList.count}" min="1"
onchange="changeCount(this)" class="form-control mr-2" >
<button type="button" class="close" aria-label="Close">
<span aria-hidden="true" th:data-id="${cartList.cartProductId}" onclick="deleteCart(this)">×</span>
</button>
</span>
</div>
</div>
</td>
<td class="text-center align-middle">
<span th:id="'totalPrice_' + ${cartList.cartProductId}"
name="totalPrice" th:text="${cartList.price * cartList.count} + '원'">
</span>
</td>
</tr>
</tbody>
</table>
<h2 class="text-center">
총 주문 금액 : <span id="orderTotal" class="text-danger">0원</span>
</h2>
<div class="text-center">
<button type="button" class="btn btn-primary btn-lg" onclick="requestPayment()">토스로 결제하기</button>
</div>
</div>
</th:block>
</html>
deleteCart
<script th:inline="javascript">
$(document).ready(function() {
// 생략
});
function deleteCart(obj){
var cartProductId = obj.dataset.id;
var url = "/cart/" + cartProductId;
$.ajax({
url : url,
type : "DELETE",
dataType : "json",
cache : false,
success : function(result, status){
location.href='/cart';
},
error : function(jqXHR, status, error){
if(jqXHR.status == '401'){
alert('로그인 후 이용해주세요');
location.href='/customer/login';
} else{
alert('시스템 오류입니다.');
}
}
});
}
</script>
반응형
'💻 my code archive > 🏷️JAVA & Spring(Boot)' 카테고리의 다른 글
스프링부트 SpringBoot 쇼핑몰 토스 Toss Payment API 연동 결제 구현하기 (1) | 2025.01.13 |
---|---|
프로젝트 #1 스프링부트 SpringBoot 3x Swagger 적용법 (1) | 2024.04.20 |
SheetJS 테이블 내용 엑셀 다운로드, 스타일 적용 방법 (0) | 2023.08.18 |
[스프링부트 블로그 만들기] 댓글 기능, 댓글 목록, 삭제까지 구현하기 (0) | 2022.03.25 |
[스프링부트 블로그 만들기] 카카오 로그인 API 서비스 구현하기 (0) | 2022.03.25 |